@adrkit/evaluator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +23 -0
  2. package/dist/LICENSE +201 -0
  3. package/dist/NOTICE +11 -0
  4. package/dist/assertions/evaluate.d.ts +36 -0
  5. package/dist/assertions/jsonpath.d.ts +20 -0
  6. package/dist/assertions/limits.d.ts +21 -0
  7. package/dist/assertions/registry.d.ts +20 -0
  8. package/dist/assertions/rego.d.ts +27 -0
  9. package/dist/catalog.d.ts +39 -0
  10. package/dist/compare.d.ts +10 -0
  11. package/dist/crypto/sha256.d.ts +10 -0
  12. package/dist/identity/directory.d.ts +23 -0
  13. package/dist/index.d.ts +24 -0
  14. package/dist/index.js +2064 -0
  15. package/dist/keys.d.ts +27 -0
  16. package/dist/pass0.d.ts +19 -0
  17. package/dist/patch/project.d.ts +20 -0
  18. package/dist/report/aggregate.d.ts +21 -0
  19. package/dist/report/assemble.d.ts +16 -0
  20. package/dist/report/order.d.ts +19 -0
  21. package/dist/report/serialize.d.ts +42 -0
  22. package/dist/routing/accepted-assertion.d.ts +12 -0
  23. package/dist/routing/route.d.ts +18 -0
  24. package/dist/routing/target.d.ts +20 -0
  25. package/dist/rules/affects-overlap.d.ts +13 -0
  26. package/dist/rules/affects-resolvable.d.ts +14 -0
  27. package/dist/rules/assertions-compile.d.ts +12 -0
  28. package/dist/rules/assertions-pass.d.ts +13 -0
  29. package/dist/rules/context.d.ts +21 -0
  30. package/dist/rules/decider-resolvable.d.ts +10 -0
  31. package/dist/rules/expiry-sane.d.ts +11 -0
  32. package/dist/rules/id-unique.d.ts +11 -0
  33. package/dist/rules/kernel.d.ts +16 -0
  34. package/dist/rules/no-orphan-refs.d.ts +12 -0
  35. package/dist/rules/schema-valid.d.ts +11 -0
  36. package/dist/rules/scope-hierarchy.d.ts +14 -0
  37. package/dist/rules/supersession-consistent.d.ts +12 -0
  38. package/dist/targets/canonical.d.ts +37 -0
  39. package/dist/targets/package.d.ts +11 -0
  40. package/dist/targets/path.d.ts +10 -0
  41. package/dist/targets/registry.d.ts +11 -0
  42. package/dist/types.d.ts +360 -0
  43. package/package.json +54 -0
  44. package/src/assertions/evaluate.ts +214 -0
  45. package/src/assertions/jsonpath.ts +95 -0
  46. package/src/assertions/limits.ts +57 -0
  47. package/src/assertions/registry.ts +38 -0
  48. package/src/assertions/rego.ts +272 -0
  49. package/src/catalog.ts +263 -0
  50. package/src/compare.ts +13 -0
  51. package/src/crypto/sha256.ts +101 -0
  52. package/src/identity/directory.ts +69 -0
  53. package/src/index.ts +81 -0
  54. package/src/keys.ts +55 -0
  55. package/src/pass0.ts +163 -0
  56. package/src/patch/project.ts +51 -0
  57. package/src/report/aggregate.ts +59 -0
  58. package/src/report/assemble.ts +43 -0
  59. package/src/report/order.ts +53 -0
  60. package/src/report/serialize.ts +152 -0
  61. package/src/routing/accepted-assertion.ts +39 -0
  62. package/src/routing/route.ts +105 -0
  63. package/src/routing/target.ts +104 -0
  64. package/src/rules/affects-overlap.ts +55 -0
  65. package/src/rules/affects-resolvable.ts +83 -0
  66. package/src/rules/assertions-compile.ts +18 -0
  67. package/src/rules/assertions-pass.ts +21 -0
  68. package/src/rules/context.ts +31 -0
  69. package/src/rules/decider-resolvable.ts +55 -0
  70. package/src/rules/expiry-sane.ts +30 -0
  71. package/src/rules/id-unique.ts +57 -0
  72. package/src/rules/kernel.ts +33 -0
  73. package/src/rules/no-orphan-refs.ts +102 -0
  74. package/src/rules/schema-valid.ts +49 -0
  75. package/src/rules/scope-hierarchy.ts +108 -0
  76. package/src/rules/supersession-consistent.ts +138 -0
  77. package/src/targets/canonical.ts +114 -0
  78. package/src/targets/package.ts +41 -0
  79. package/src/targets/path.ts +32 -0
  80. package/src/targets/registry.ts +23 -0
  81. package/src/types.ts +445 -0
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @adrkit/evaluator — rule 7: scope-hierarchy (error).
3
+ *
4
+ * A `component` proposal that overlaps an APPLICABLE accepted `org` ADR whose assertion
5
+ * was GREEN on the supplied base input and FAILS on the supplied proposed input is an
6
+ * attributable contradiction ⇒ error (research §R5). Domain applicability is explicit:
7
+ * an org ADR with no domain is global; otherwise it applies only on exact domain
8
+ * equality. The evaluator itself compiles + evaluates the accepted assertion (it never
9
+ * accepts a precomputed verdict). Missing engine/source/base/proposed evidence is inert;
10
+ * a non-component proposal or one with no applicable org overlap passes.
11
+ */
12
+
13
+ import type { Adr } from '@adrkit/core';
14
+ import { aggregate, passResult, type SubResult } from './kernel.ts';
15
+ import type { RuleContext } from './context.ts';
16
+ import { compileAssertionForScope } from '../assertions/evaluate.ts';
17
+ import { makeAssertionKey } from '../keys.ts';
18
+ import { anyMatcherInert, resolveRecordTargets } from '../targets/canonical.ts';
19
+ import { byCodeUnit } from '../compare.ts';
20
+ import type { RuleResult } from '../types.ts';
21
+
22
+ function domainApplies(orgRecord: Adr, proposal: Adr): boolean {
23
+ const orgDomain = orgRecord.frontmatter.domain;
24
+ if (orgDomain === undefined) return true; // org-with-no-domain is global
25
+ return orgDomain === proposal.frontmatter.domain;
26
+ }
27
+
28
+ export function evaluateScopeHierarchy(ctx: RuleContext): RuleResult {
29
+ const proposal = ctx.proposed;
30
+ if (proposal.frontmatter.scope !== 'component') {
31
+ return passResult('scope-hierarchy', 'scope-hierarchy.not-applicable-scope');
32
+ }
33
+
34
+ const proposalTargets = resolveRecordTargets(proposal, ctx.input.targetRegistry, ctx.input.targets, ctx.input.resolutionLog);
35
+ const subs: SubResult[] = [];
36
+ if (anyMatcherInert(proposalTargets)) {
37
+ subs.push({ status: 'inert', reason: 'scope-hierarchy.evidence-absent' });
38
+ }
39
+
40
+ const applicableOrgAdrs: Adr[] = [];
41
+ for (const record of ctx.acceptedRecords) {
42
+ if (record.frontmatter.scope !== 'org') continue;
43
+ if (!domainApplies(record, proposal)) continue;
44
+ const orgTargets = resolveRecordTargets(record, ctx.input.targetRegistry, ctx.input.targets, ctx.input.resolutionLog);
45
+ if (anyMatcherInert(orgTargets)) {
46
+ subs.push({ status: 'inert', reason: 'scope-hierarchy.evidence-absent' });
47
+ }
48
+ if ([...proposalTargets.targetKeys].some((key) => orgTargets.targetKeys.has(key))) {
49
+ applicableOrgAdrs.push(record);
50
+ }
51
+ }
52
+
53
+ if (applicableOrgAdrs.length === 0) {
54
+ return subs.length > 0
55
+ ? aggregate('scope-hierarchy', subs)
56
+ : passResult('scope-hierarchy', 'scope-hierarchy.ok');
57
+ }
58
+
59
+ const transitions: string[] = [];
60
+
61
+ for (const orgRecord of applicableOrgAdrs) {
62
+ for (const assertion of orgRecord.frontmatter.assertions) {
63
+ const key = makeAssertionKey(orgRecord.log, orgRecord.path, assertion.id);
64
+ const compiled = compileAssertionForScope(ctx, orgRecord, assertion);
65
+ if (!compiled.ok) {
66
+ const reason =
67
+ compiled.reason === 'engine-absent'
68
+ ? 'scope-hierarchy.engine-absent'
69
+ : 'scope-hierarchy.source-absent';
70
+ subs.push({ status: 'inert', reason });
71
+ continue;
72
+ }
73
+ const baseInput = ctx.input.scopeEvidence?.baseInputs?.[key]?.document;
74
+ if (baseInput === undefined) {
75
+ subs.push({ status: 'inert', reason: 'scope-hierarchy.base-input-absent' });
76
+ continue;
77
+ }
78
+ const proposedInput = ctx.input.assertionInputs.inputs[key]?.document;
79
+ if (proposedInput === undefined) {
80
+ subs.push({ status: 'inert', reason: 'scope-hierarchy.proposed-input-absent' });
81
+ continue;
82
+ }
83
+ const baseEval = compiled.evaluate(baseInput);
84
+ const proposedEval = compiled.evaluate(proposedInput);
85
+ if (!baseEval.ok || !proposedEval.ok) {
86
+ subs.push({ status: 'inert', reason: 'scope-hierarchy.evidence-absent' });
87
+ continue;
88
+ }
89
+ if (baseEval.pass && !proposedEval.pass) {
90
+ transitions.push(`${orgRecord.frontmatter.id}:${assertion.id}`);
91
+ subs.push({
92
+ status: 'fail',
93
+ reason: 'scope-hierarchy.contradicts-org-assertion',
94
+ finding: {
95
+ reason: 'scope-hierarchy.contradicts-org-assertion',
96
+ candidateAdr: proposal.frontmatter.id,
97
+ relatedAdr: orgRecord.frontmatter.id,
98
+ assertionKey: key,
99
+ message: `Proposal turns accepted org assertion "${orgRecord.frontmatter.id}:${assertion.id}" from green to red`,
100
+ },
101
+ });
102
+ }
103
+ }
104
+ }
105
+
106
+ if (subs.length === 0) return passResult('scope-hierarchy', 'scope-hierarchy.ok');
107
+ return aggregate('scope-hierarchy', subs, transitions.length > 0 ? { assertionTransitions: [...transitions].sort(byCodeUnit) } : undefined);
108
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @adrkit/evaluator — rule 3: supersession-consistent (error).
3
+ *
4
+ * New deterministic reciprocity + cycle checks over the corpus snapshot (research
5
+ * §R3), reusing `buildAdrGraph` for EDGES ONLY. Reciprocity: `A supersedes B` iff
6
+ * `B.supersededBy === A`. `dangling-supersededBy` is owned exclusively by this rule
7
+ * (C2) and suppresses the redundant non-reciprocal finding for the same ref. Cycles
8
+ * in the supersedes relation fail with `cycle`. Exactly one aggregate result (C11).
9
+ */
10
+
11
+ import { buildAdrGraph, type Adr } from '@adrkit/core';
12
+ import { aggregate, passResult, type SubResult } from './kernel.ts';
13
+ import type { RuleContext } from './context.ts';
14
+ import type { RuleFinding, RuleResult } from '../types.ts';
15
+
16
+ function hasSupersedesCycle(records: readonly Adr[]): boolean {
17
+ const graph = buildAdrGraph(records);
18
+ const adjacency = new Map<string, string[]>();
19
+ for (const edge of graph.edges) {
20
+ if (edge.kind !== 'supersedes') continue;
21
+ const list = adjacency.get(edge.from) ?? [];
22
+ list.push(edge.to);
23
+ adjacency.set(edge.from, list);
24
+ }
25
+ const WHITE = 0;
26
+ const GRAY = 1;
27
+ const BLACK = 2;
28
+ const color = new Map<string, number>();
29
+ const nodes = graph.nodes.map((node) => node.id);
30
+
31
+ function visit(node: string): boolean {
32
+ color.set(node, GRAY);
33
+ for (const next of adjacency.get(node) ?? []) {
34
+ const state = color.get(next) ?? WHITE;
35
+ if (state === GRAY) return true;
36
+ if (state === WHITE && visit(next)) return true;
37
+ }
38
+ color.set(node, BLACK);
39
+ return false;
40
+ }
41
+
42
+ for (const node of nodes) {
43
+ if ((color.get(node) ?? WHITE) === WHITE && visit(node)) return true;
44
+ }
45
+ return false;
46
+ }
47
+
48
+ export function evaluateSupersessionConsistent(ctx: RuleContext): RuleResult {
49
+ const records = ctx.corpusRecords;
50
+ const ids = new Set(records.map((record) => record.frontmatter.id));
51
+ const supersededByById = new Map<string, string>();
52
+ for (const record of records) {
53
+ if (record.frontmatter.supersededBy) {
54
+ supersededByById.set(record.frontmatter.id, record.frontmatter.supersededBy);
55
+ }
56
+ }
57
+
58
+ const subs: SubResult[] = [];
59
+ const seenPairs = new Set<string>();
60
+ const danglingRefs = new Set<string>();
61
+
62
+ // Dangling supersededBy — owned here, once per record.
63
+ for (const record of records) {
64
+ const target = record.frontmatter.supersededBy;
65
+ if (target && !ids.has(target)) {
66
+ danglingRefs.add(`${record.frontmatter.id}->${target}`);
67
+ const finding: RuleFinding = {
68
+ reason: 'supersession-consistent.dangling-superseded-by',
69
+ message: `supersededBy "${target}" does not resolve to a record in the corpus`,
70
+ adr: record.frontmatter.id,
71
+ candidateAdr: record.frontmatter.id,
72
+ relatedAdr: target,
73
+ recordPath: record.path,
74
+ field: 'supersededBy',
75
+ lowerLevel: { rule: 'dangling-supersededBy', path: record.path, id: record.frontmatter.id, field: 'supersededBy' },
76
+ };
77
+ subs.push({ status: 'fail', reason: 'supersession-consistent.dangling-superseded-by', finding });
78
+ }
79
+ }
80
+
81
+ function nonReciprocal(fromId: string, toId: string, path: string, field: 'supersedes' | 'supersededBy'): void {
82
+ const key = [fromId, toId].sort().join('|');
83
+ if (seenPairs.has(key)) return;
84
+ seenPairs.add(key);
85
+ const finding: RuleFinding = {
86
+ reason: 'supersession-consistent.non-reciprocal',
87
+ message: `supersedes/supersededBy between "${fromId}" and "${toId}" is not reciprocal`,
88
+ adr: fromId,
89
+ candidateAdr: fromId,
90
+ relatedAdr: toId,
91
+ recordPath: path,
92
+ field,
93
+ lowerLevel: { rule: 'non-reciprocal', path, id: fromId, field },
94
+ };
95
+ subs.push({ status: 'fail', reason: 'supersession-consistent.non-reciprocal', finding });
96
+ }
97
+
98
+ // A supersedes B ⇒ B.supersededBy === A.
99
+ for (const record of records) {
100
+ for (const target of record.frontmatter.supersedes) {
101
+ if (!ids.has(target)) continue; // orphan supersedes — owned by no-orphan-refs
102
+ if (supersededByById.get(target) !== record.frontmatter.id) {
103
+ nonReciprocal(record.frontmatter.id, target, record.path, 'supersedes');
104
+ }
105
+ }
106
+ }
107
+
108
+ // B.supersededBy === A ⇒ A.supersedes contains B.
109
+ const supersedesPairs = new Set<string>();
110
+ for (const record of records) {
111
+ for (const target of record.frontmatter.supersedes) {
112
+ supersedesPairs.add(`${record.frontmatter.id}=>${target}`);
113
+ }
114
+ }
115
+ for (const record of records) {
116
+ const target = record.frontmatter.supersededBy;
117
+ if (!target || !ids.has(target)) continue; // dangling handled above
118
+ if (danglingRefs.has(`${record.frontmatter.id}->${target}`)) continue;
119
+ if (!supersedesPairs.has(`${target}=>${record.frontmatter.id}`)) {
120
+ nonReciprocal(record.frontmatter.id, target, record.path, 'supersededBy');
121
+ }
122
+ }
123
+
124
+ if (hasSupersedesCycle(records)) {
125
+ subs.push({
126
+ status: 'fail',
127
+ reason: 'supersession-consistent.cycle',
128
+ finding: {
129
+ reason: 'supersession-consistent.cycle',
130
+ message: 'The supersedes relation contains a cycle',
131
+ lowerLevel: { rule: 'supersedes-cycle' },
132
+ },
133
+ });
134
+ }
135
+
136
+ if (subs.length === 0) return passResult('supersession-consistent');
137
+ return aggregate('supersession-consistent', subs);
138
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * @adrkit/evaluator — canonical target normalization + record resolution (T035, R4/ADR-0009).
3
+ *
4
+ * Resolves a record's `affects` matchers to a finite canonical target-id set, honoring
5
+ * ADR-0009 semantics: union non-negated matches, subtract matching negations, require
6
+ * at least one positive matcher (negation-only ⇒ empty), and apply a `repo` qualifier
7
+ * only when it equals the caller-supplied current resolution `log` (a different-repo
8
+ * qualifier contributes no local match). The current log is passed explicitly to every
9
+ * resolver port; it is never inferred from a record's source `log`.
10
+ */
11
+
12
+ import type { Adr, AffectsType } from '@adrkit/core';
13
+ import { canonicalTargetKey } from '../keys.ts';
14
+ import { byCodeUnit } from '../compare.ts';
15
+ import type {
16
+ CanonicalTargetId,
17
+ CanonicalTargetKey,
18
+ TargetInventorySnapshots,
19
+ TargetResolutionContext,
20
+ TargetResolutionRegistry,
21
+ } from '../types.ts';
22
+
23
+ /** Normalize a repo-relative path id to posix form (no leading `./`, no backslashes). */
24
+ export function normalizePathId(path: string): string {
25
+ return path.replace(/\\/g, '/').replace(/^\.\//, '');
26
+ }
27
+
28
+ export function makeTargetId(kind: AffectsType, id: string): CanonicalTargetId {
29
+ return { kind, id: kind === 'path' ? normalizePathId(id) : id };
30
+ }
31
+
32
+ /** The outcome of resolving a single applied `affects` matcher. */
33
+ export interface MatcherResolution {
34
+ readonly type: AffectsType;
35
+ readonly pattern: string;
36
+ readonly negate: boolean;
37
+ /** `resolved` includes a different-repo matcher (deterministic empty locally). */
38
+ readonly status: 'resolved' | 'inert-backing' | 'inert-resolver';
39
+ readonly ids: readonly CanonicalTargetId[];
40
+ }
41
+
42
+ export interface RecordTargetResolution {
43
+ /** Final, unique, canonically-ordered target ids (positive − negated). */
44
+ readonly targets: readonly CanonicalTargetId[];
45
+ readonly targetKeys: ReadonlySet<CanonicalTargetKey>;
46
+ /** Per-applied-matcher outcomes, so each matcher can be validated independently (C3, finding #2). */
47
+ readonly matchers: readonly MatcherResolution[];
48
+ readonly hasMatchers: boolean;
49
+ readonly hasPositiveMatcher: boolean;
50
+ }
51
+
52
+ /** True if any matcher could not be resolved (missing inventory or resolver port). */
53
+ export function anyMatcherInert(resolution: RecordTargetResolution): boolean {
54
+ return resolution.matchers.some((matcher) => matcher.status !== 'resolved');
55
+ }
56
+
57
+ function sortedUnique(ids: Iterable<CanonicalTargetId>): CanonicalTargetId[] {
58
+ const byKey = new Map<CanonicalTargetKey, CanonicalTargetId>();
59
+ for (const id of ids) byKey.set(canonicalTargetKey(id), id);
60
+ return [...byKey.values()].sort((a, b) => byCodeUnit(canonicalTargetKey(a), canonicalTargetKey(b)));
61
+ }
62
+
63
+ /** Resolve one record's affects matchers against the registry + inventory. */
64
+ export function resolveRecordTargets(
65
+ record: Adr,
66
+ registry: TargetResolutionRegistry,
67
+ inventory: TargetInventorySnapshots,
68
+ resolutionLog: string | undefined,
69
+ ): RecordTargetResolution {
70
+ const context: TargetResolutionContext = { ...(resolutionLog !== undefined ? { log: resolutionLog } : {}), inventory };
71
+ const positive = new Map<CanonicalTargetKey, CanonicalTargetId>();
72
+ const negated = new Set<CanonicalTargetKey>();
73
+ const matcherResolutions: MatcherResolution[] = [];
74
+ const matchers = record.frontmatter.affects;
75
+ let hasPositiveMatcher = false;
76
+
77
+ for (const matcher of matchers) {
78
+ const type = matcher.type;
79
+ if (!matcher.negate) hasPositiveMatcher = true;
80
+
81
+ // repo qualifier: a different-repo matcher applies to another repo and contributes no
82
+ // local match — it resolves deterministically to the empty set locally.
83
+ if (matcher.repo !== undefined && matcher.repo !== resolutionLog) {
84
+ matcherResolutions.push({ type, pattern: matcher.pattern, negate: matcher.negate, status: 'resolved', ids: [] });
85
+ continue;
86
+ }
87
+ const port = registry.get(type);
88
+ if (!port) {
89
+ matcherResolutions.push({ type, pattern: matcher.pattern, negate: matcher.negate, status: 'inert-resolver', ids: [] });
90
+ continue;
91
+ }
92
+ const resolution = port.resolve(matcher, context);
93
+ if (resolution.status === 'inert') {
94
+ matcherResolutions.push({ type, pattern: matcher.pattern, negate: matcher.negate, status: 'inert-backing', ids: [] });
95
+ continue;
96
+ }
97
+ matcherResolutions.push({ type, pattern: matcher.pattern, negate: matcher.negate, status: 'resolved', ids: resolution.ids });
98
+ for (const id of resolution.ids) {
99
+ const key = canonicalTargetKey(id);
100
+ if (matcher.negate) negated.add(key);
101
+ else positive.set(key, id);
102
+ }
103
+ }
104
+
105
+ const finalIds = [...positive.entries()].filter(([key]) => !negated.has(key)).map(([, id]) => id);
106
+ const targets = sortedUnique(finalIds);
107
+ return {
108
+ targets,
109
+ targetKeys: new Set(targets.map(canonicalTargetKey)),
110
+ matchers: matcherResolutions,
111
+ hasMatchers: matchers.length > 0,
112
+ hasPositiveMatcher,
113
+ };
114
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @adrkit/evaluator — built-in `package` target resolver.
3
+ *
4
+ * Reuses core's neutral `matchPackagePattern` grammar (name + optional semver range)
5
+ * over a complete dependency inventory, per dependency — without copying the grammar
6
+ * or importing semver directly (that stays a core-internal concern). A missing
7
+ * `dependencies` inventory is inert (backing-absent); an unparseable pattern resolves
8
+ * to the empty set. Pure.
9
+ */
10
+
11
+ import { matchPackagePattern, parsePackagePattern } from '@adrkit/core';
12
+ import { makeTargetId } from './canonical.ts';
13
+ import type { AffectsMatcher, TargetResolution, TargetResolutionContext, TargetResolverPort } from '../types.ts';
14
+
15
+ export function createPackageTargetResolver(): TargetResolverPort {
16
+ return {
17
+ type: 'package',
18
+ resolve(matcher: AffectsMatcher, context: TargetResolutionContext): TargetResolution {
19
+ const inventory = context.inventory.dependencies;
20
+ if (inventory === undefined) {
21
+ return { status: 'inert', ids: [], reason: 'affects-resolvable.backing-absent' };
22
+ }
23
+ if (!parsePackagePattern(matcher.pattern)) {
24
+ return { status: 'resolved', ids: [], reason: 'affects-resolvable.zero-targets' };
25
+ }
26
+ const ids = inventory
27
+ .filter(
28
+ (dependency) =>
29
+ matchPackagePattern(matcher.pattern, [
30
+ { name: dependency.name, version: dependency.version ?? '0.0.0-unversioned' },
31
+ ]).matched,
32
+ )
33
+ .map((dependency) => makeTargetId('package', dependency.name));
34
+ return {
35
+ status: 'resolved',
36
+ ids,
37
+ reason: ids.length > 0 ? 'affects-resolvable.ok' : 'affects-resolvable.zero-targets',
38
+ };
39
+ },
40
+ };
41
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @adrkit/evaluator — built-in `path` target resolver.
3
+ *
4
+ * Reuses core's neutral repo-relative path-glob primitive (`matchPathPattern`) over a
5
+ * complete tracked-path inventory — it never copies the grammar. A missing
6
+ * `trackedPaths` inventory is inert (backing-absent); a bad/leading-slash pattern
7
+ * resolves to the empty set (⇒ affects-resolvable zero-targets, not a throw). Pure.
8
+ */
9
+
10
+ import { matchPathPattern } from '@adrkit/core';
11
+ import { makeTargetId } from './canonical.ts';
12
+ import type { AffectsMatcher, TargetResolution, TargetResolutionContext, TargetResolverPort } from '../types.ts';
13
+
14
+ export function createPathTargetResolver(): TargetResolverPort {
15
+ return {
16
+ type: 'path',
17
+ resolve(matcher: AffectsMatcher, context: TargetResolutionContext): TargetResolution {
18
+ const inventory = context.inventory.trackedPaths;
19
+ if (inventory === undefined) {
20
+ return { status: 'inert', ids: [], reason: 'affects-resolvable.backing-absent' };
21
+ }
22
+ const ids = inventory
23
+ .filter((path) => matchPathPattern(matcher.pattern, [path]).matched)
24
+ .map((path) => makeTargetId('path', path));
25
+ return {
26
+ status: 'resolved',
27
+ ids,
28
+ reason: ids.length > 0 ? 'affects-resolvable.ok' : 'affects-resolvable.zero-targets',
29
+ };
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @adrkit/evaluator — target resolver registry.
3
+ *
4
+ * A registry is a pure lookup from `AffectsType` to a deterministic resolver port.
5
+ * A lookup miss makes the corresponding matcher inert (`resolver-absent`), never a
6
+ * failure. Ports are built only by trusted composition code — never selected by JSON.
7
+ */
8
+
9
+ import type { AffectsType } from '@adrkit/core';
10
+ import type { TargetResolutionRegistry, TargetResolverPort } from '../types.ts';
11
+
12
+ export function createTargetResolutionRegistry(
13
+ ports: readonly TargetResolverPort[],
14
+ ): TargetResolutionRegistry {
15
+ const byType = new Map<AffectsType, TargetResolverPort>();
16
+ for (const port of ports) byType.set(port.type, port);
17
+ return { get: (type: AffectsType) => byType.get(type) };
18
+ }
19
+
20
+ /** A registry with no resolvers — every target matcher is `resolver-absent` inert. */
21
+ export const emptyTargetResolutionRegistry: TargetResolutionRegistry = {
22
+ get: () => undefined,
23
+ };