/*
Runtime resolver for RFC-0183 Feature Policy embedded in RFC-0047 content domains.
- Do not import from astro:content or other framework-specific modules.
- Do not modify content files.
- Do not handle remote feature flag services.
- RFC-0183: Initial runtime resolver for Feature Policy without Feature Graph dependency.
- RFC-1097: step 6 — compass.migrate codemod run
Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order.
- RFC-1097: sweep — werkstatt-engine clean
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.
*/
import type {
FeaturePolicy,
FeaturePolicyBehaviorValue,
FeaturePolicyTargetRef,
FeaturePolicyVisibility,
ResolvedFeaturePolicy,
} from "../schemas/features.ts";
// Re-export types for consumers
export type {
FeaturePolicy,
FeaturePolicyBehaviorValue,
FeaturePolicyTargetKind,
FeaturePolicyTargetRef,
FeaturePolicyVisibility,
ResolvedFeaturePolicy,
} from "../schemas/features.ts";
export interface FeaturePolicyResolverOptions {
/**
* Default language code for policy resolution when localized policy absent.
*/
defaultLanguage?: string;
/**
* Whether to cache resolved policy. Default true.
*/
cache?: boolean;
/**
* Platform default visibility when no policy found anywhere.
* RFC-0183 default is "enabled".
*/
platformDefaultVisibility?: FeaturePolicyVisibility;
}
/**
* Narrow interface for content nodes that may carry policy.
* Resolver works with any object matching this shape, allowing
* framework-neutral resolution from Astro content, plain objects, etc.
*/
export interface PolicyCarryingNode {
id?: string;
policy?: FeaturePolicy;
blocks?: PolicyCarryingNode[];
items?: PolicyCarryingNode[];
components?: PolicyCarryingNode[];
[key: string]: unknown;
}
/**
* Content context passed to resolver.
* Contains already-loaded content from RFC-0047 domains.
*/
export interface FeaturePolicyContentContext {
/** Site-level policy defaults from system.md */
systemPolicy?: FeaturePolicy;
/** Site-level defaults from site/{lang}/config or similar */
siteDefaults?: FeaturePolicy;
/** Current page content node */
page?: PolicyCarryingNode;
/** Current language */
lang?: string;
/** Default language for fallback */
defaultLanguage?: string;
}
// Resolution is pure given (context, target, lang, platformDefault). The cache is
// therefore scoped to the content CONTEXT — a WeakMap keyed by context identity — so two
// different contexts never share resolved policy for the same target+lang.
// Bug history (RFC-0183): a module-global array keyed by target+lang ALONE silently
// returned one context's result for another, clobbering page-level policy. Do not
// reintroduce a context-agnostic global cache.
const MAX_CACHE_SIZE = 100;
let contextCaches = new WeakMap>();
function getCacheKey(
target: FeaturePolicyTargetRef,
lang: string,
platformDefault: FeaturePolicyVisibility,
): string {
const parts = [target.kind, lang, `d:${platformDefault}`];
if (target.pageId) parts.push(`p:${target.pageId}`);
if (target.blockId) parts.push(`b:${target.blockId}`);
if (target.componentId) parts.push(`c:${target.componentId}`);
if (target.itemId) parts.push(`i:${target.itemId}`);
return parts.join("|");
}
function getCached(
context: FeaturePolicyContentContext,
key: string,
): ResolvedFeaturePolicy | undefined {
return contextCaches.get(context)?.get(key);
}
function setCached(
context: FeaturePolicyContentContext,
key: string,
result: ResolvedFeaturePolicy,
): void {
let cache = contextCaches.get(context);
if (!cache) {
cache = new Map();
contextCaches.set(context, cache);
}
// Evict oldest (Map preserves insertion order) when at capacity.
if (cache.size >= MAX_CACHE_SIZE) {
const oldest = cache.keys().next().value;
if (oldest !== undefined) cache.delete(oldest);
}
cache.set(key, result);
}
/** Drops all cached resolutions. Per-context caches are also GC'd with their context. */
export function clearFeaturePolicyCache(): void {
contextCaches = new WeakMap();
}
/**
* Search for policy in a node tree by id path.
* Handles nested structures: page → blocks → components → items.
*/
function findPolicyInNode(
node: PolicyCarryingNode,
target: FeaturePolicyTargetRef,
): FeaturePolicy | null {
// Direct match on current node
if (
(target.blockId && node.id === target.blockId) ||
(target.componentId && node.id === target.componentId) ||
(target.itemId && node.id === target.itemId)
) {
return node.policy ?? null;
}
// Search in blocks
if (target.blockId && node.blocks) {
for (const block of node.blocks) {
if (block.id === target.blockId) {
// If looking for nested component/item within this block
if (target.componentId && block.components) {
const comp = block.components.find((c) => c.id === target.componentId);
if (comp) {
if (target.itemId && comp.items) {
const item = comp.items.find((i) => i.id === target.itemId);
if (item?.policy) return item.policy;
}
return comp.policy ?? null;
}
}
return block.policy ?? null;
}
// Deep search if not matched by id
const deep = findPolicyInNode(block, target);
if (deep) return deep;
}
}
// Search in items (for shared components)
if (target.itemId && node.items) {
const item = node.items.find((i) => i.id === target.itemId);
if (item?.policy) return item.policy;
}
// Search in components
if (target.componentId && node.components) {
const comp = node.components.find((c) => c.id === target.componentId);
if (comp) {
if (target.itemId && comp.items) {
const item = comp.items.find((i) => i.id === target.itemId);
if (item?.policy) return item.policy;
}
return comp.policy ?? null;
}
}
// Recursive search in any nested structure
const nestedArrays = [node.blocks, node.components, node.items].filter(Boolean);
for (const arr of nestedArrays) {
for (const child of arr ?? []) {
const found = findPolicyInNode(child, target);
if (found) return found;
}
}
return null;
}
/**
* RFC-0183 resolution order:
* 1. Explicit policy on content node
* 2. Page-level policy (via page content context)
* 3. System-level policy for matching ids
* 4. Site-level defaults
* 5. Platform default: visibility=enabled, empty behavior
*/
function resolvePolicyInternal(
target: FeaturePolicyTargetRef,
context: FeaturePolicyContentContext,
platformDefault: FeaturePolicyVisibility,
): ResolvedFeaturePolicy {
const lang = target.lang ?? context.lang;
const defaultLang = context.defaultLanguage;
if (!lang || !defaultLang) {
throw new Error("[feature-policy] lang and defaultLanguage are required.");
}
// 1. Try explicit node policy via page context
let foundPolicy: FeaturePolicy | null = null;
let inheritedFrom: FeaturePolicyTargetRef | undefined;
if (context.page) {
foundPolicy = findPolicyInNode(context.page, target);
if (foundPolicy) {
inheritedFrom = { ...target, lang };
}
}
// 2. Try system-level policy if id-based lookup exists there
// (System policy stored as flat record keyed by "page:home/block:hero")
if (!foundPolicy && context.systemPolicy) {
const systemKey = buildSystemKey(target);
if (systemKey && (context.systemPolicy as Record)[systemKey]) {
foundPolicy = (context.systemPolicy as Record)[systemKey];
inheritedFrom = { kind: "page" as const, lang: defaultLang }; // system is lang-agnostic
}
}
// 3. Site-level defaults
if (!foundPolicy && context.siteDefaults) {
foundPolicy = context.siteDefaults;
inheritedFrom = { kind: "page" as const, lang: defaultLang };
}
// 4. Build resolved result with defaults
const resolved: ResolvedFeaturePolicy = {
target,
visibility: foundPolicy?.visibility ?? platformDefault,
behavior: foundPolicy?.behavior ?? {},
sourcePath: buildSourcePath(foundPolicy, inheritedFrom),
inheritedFrom,
};
return resolved;
}
function buildSystemKey(target: FeaturePolicyTargetRef): string | null {
const parts: string[] = [];
if (target.pageId) parts.push(`page:${target.pageId}`);
if (target.blockId) parts.push(`block:${target.blockId}`);
if (target.componentId) parts.push(`component:${target.componentId}`);
if (target.itemId) parts.push(`item:${target.itemId}`);
return parts.length > 0 ? parts.join("/") : null;
}
function buildSourcePath(policy: FeaturePolicy | null, inherited?: FeaturePolicyTargetRef): string {
if (!policy) return "platform-default";
if (inherited) {
const parts: string[] = [inherited.kind];
if (inherited.pageId) parts.push(inherited.pageId);
if (inherited.blockId) parts.push(inherited.blockId);
if (inherited.componentId) parts.push(inherited.componentId);
if (inherited.itemId) parts.push(inherited.itemId);
return parts.join("/");
}
return "node-explicit";
}
/**
* Resolve feature policy for a target.
* Resolution is synchronous given loaded content context.
* Kept as the canonical name; resolveFeaturePolicySync is an alias.
*/
export function resolveFeaturePolicy(
target: FeaturePolicyTargetRef,
context: FeaturePolicyContentContext,
options: FeaturePolicyResolverOptions = {},
): ResolvedFeaturePolicy {
const { cache = true, platformDefaultVisibility = "enabled" } = options;
const lang = target.lang ?? context.lang;
if (!lang) {
throw new Error("[feature-policy] lang is required.");
}
if (cache) {
const key = getCacheKey(target, lang, platformDefaultVisibility);
const cached = getCached(context, key);
if (cached) return cached;
const result = resolvePolicyInternal(target, context, platformDefaultVisibility);
setCached(context, key, result);
return result;
}
return resolvePolicyInternal(target, context, platformDefaultVisibility);
}
/**
* Alias for resolveFeaturePolicy (kept for call sites that explicitly want the
* synchronous signature). Resolution was always synchronous.
*/
export function resolveFeaturePolicySync(
target: FeaturePolicyTargetRef,
context: FeaturePolicyContentContext,
options: FeaturePolicyResolverOptions = {},
): ResolvedFeaturePolicy {
return resolveFeaturePolicy(target, context, options);
}
/**
* Factory for creating a configured resolver bound to specific content context.
*/
export function createFeaturePolicyResolver(
context: FeaturePolicyContentContext,
options: FeaturePolicyResolverOptions = {},
) {
return {
resolve: (target: FeaturePolicyTargetRef) => resolveFeaturePolicy(target, context, options),
resolveSync: (target: FeaturePolicyTargetRef) =>
resolveFeaturePolicySync(target, context, options),
isEnabled: (target: FeaturePolicyTargetRef) =>
isPolicyEnabled(resolveFeaturePolicySync(target, context, options)),
getBehavior: (
target: FeaturePolicyTargetRef,
key: string,
defaultValue?: FeaturePolicyBehaviorValue,
) => getPolicyBehavior(resolveFeaturePolicySync(target, context, options), key, defaultValue),
};
}
/**
* Predicate: is the resolved policy effectively enabled?
* Treats "enabled" as true, "disabled"/"hidden" as false, "draft" depends on environment.
*/
export function isPolicyEnabled(policy: ResolvedFeaturePolicy, isDraftAllowed = false): boolean {
if (policy.visibility === "enabled") return true;
if (policy.visibility === "disabled" || policy.visibility === "hidden") return false;
if (policy.visibility === "draft") return isDraftAllowed;
return true; // unknown defaults to enabled per RFC-0183
}
/**
* Extract behavior value with optional default.
*/
export function getPolicyBehavior(
policy: ResolvedFeaturePolicy,
key: string,
defaultValue?: FeaturePolicyBehaviorValue,
): FeaturePolicyBehaviorValue {
const value = policy.behavior[key];
if (value === undefined) return defaultValue ?? null;
return value as FeaturePolicyBehaviorValue;
}