yarramate 1.7.0 → 1.9.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 (37) hide show
  1. package/dist/adapters/visual/session-server.js +3 -0
  2. package/dist/adapters/visual/workspace-model.d.ts +4 -0
  3. package/dist/adapters/visual/workspace-model.js +1 -1
  4. package/dist/artifact-coverage.d.ts +13 -0
  5. package/dist/artifact-coverage.js +58 -0
  6. package/dist/ask-command.js +3 -3
  7. package/dist/cli.js +6 -1
  8. package/dist/compiler.d.ts +23 -0
  9. package/dist/compiler.js +23 -1
  10. package/dist/design-command.js +1 -1
  11. package/dist/index.d.ts +3 -2
  12. package/dist/index.js +1 -0
  13. package/dist/interrogate-command.d.ts +36 -1
  14. package/dist/interrogate-command.js +26 -6
  15. package/dist/interrogation-entry.d.ts +1 -1
  16. package/dist/reconciliation.d.ts +21 -1
  17. package/dist/reconciliation.js +93 -2
  18. package/dist/visual-app/assets/index-1MLdJYji.css +1 -0
  19. package/dist/visual-app/assets/index-DsH9ve0w.js +394 -0
  20. package/dist/visual-app/index.html +2 -2
  21. package/dist/visual-app-lib/editor.js +25859 -26143
  22. package/dist/visual-app-lib/styles.css +1 -1
  23. package/dist/visual-app-lib/types/adapters/visual/workspace-model.d.ts +4 -0
  24. package/dist/visual-app-lib/types/compiler.d.ts +23 -0
  25. package/dist/visual-app-lib/types/interrogate-command.d.ts +36 -1
  26. package/dist/visual-app-lib/types/visual-app/connection-panel.d.ts +25 -1
  27. package/dist/visual-app-lib/types/visual-app/local-host.d.ts +11 -1
  28. package/dist/visual-app-lib/types/workspace.d.ts +8 -0
  29. package/dist/workspace.d.ts +8 -0
  30. package/dist/workspace.js +35 -13
  31. package/docs/CONSUMING-YARRAMATE.md +14 -0
  32. package/package.json +1 -1
  33. package/schema/yarramate-question-catalogue.schema.json +30 -0
  34. package/schema/yarramate-reconciliation-report.schema.json +21 -1
  35. package/schema/yarramate-workspace.schema.json +4 -0
  36. package/dist/visual-app/assets/index-C42oLqXj.js +0 -394
  37. package/dist/visual-app/assets/index-Rkq6smL2.css +0 -1
@@ -620,6 +620,9 @@ export const startVisualServer = async (options) => {
620
620
  compiledWorkspace = {
621
621
  graph: compiled.graph,
622
622
  profileContext: compiled.profileContext,
623
+ // Threaded whole (ADR 0131): a narrow copy here is how a slot
624
+ // question would silently never fire in the embedded pane.
625
+ patternMemberships: compiled.patternMemberships,
623
626
  };
624
627
  const workspaceModel = renderedWorkspaceOf(compiledWorkspace, views, {
625
628
  authority: rendered.authority,
@@ -4,6 +4,7 @@ import type { ResolvedWorkspace } from "../../workspace.js";
4
4
  import type { VisualDiagnostic, VisualViewOperation } from "./protocol-contract.js";
5
5
  import type { ProjectionDefinition, ProjectionExclusion, ProjectionQuery } from "../../projection.js";
6
6
  import type { ResolvedProfileContext, SemanticGraph } from "../../compiler.js";
7
+ import { type CataloguePatternMembership } from "../../interrogate-command.js";
7
8
  import type { VisualKindOption, VisualViewSummary } from "./protocol-contract.js";
8
9
  import type { VisualInterrogationOverlay, VisualRenderedModel } from "./wire.js";
9
10
  /**
@@ -57,6 +58,8 @@ export interface DismissedQuestion {
57
58
  export declare const interrogationOverlayOf: (compiled: {
58
59
  readonly graph: SemanticGraph;
59
60
  readonly profileContext: ResolvedProfileContext;
61
+ /** From the compilation (ADR 0131); absent, slot questions stay quiet. */
62
+ readonly patternMemberships?: readonly CataloguePatternMembership[];
60
63
  },
61
64
  /**
62
65
  * The catalogue, or the composed SET a workspace carries (#345, ADR 0129).
@@ -90,6 +93,7 @@ dismissed?: readonly DismissedQuestion[]) => VisualInterrogationOverlay | undefi
90
93
  export declare const renderedWorkspaceOf: (compiled: {
91
94
  readonly graph: SemanticGraph;
92
95
  readonly profileContext: ResolvedProfileContext;
96
+ readonly patternMemberships?: readonly CataloguePatternMembership[];
93
97
  }, views: readonly VisualViewSummary[], metadata: Omit<VisualRenderedModel, "graph" | "vocabulary" | "interrogation">, catalogue?: {
94
98
  readonly path: string;
95
99
  readonly source: string;
@@ -64,7 +64,7 @@ dismissed = []) => {
64
64
  const composed = composeCatalogues(Array.isArray(catalogue) ? catalogue : [catalogue]);
65
65
  if (!composed.ok)
66
66
  return undefined;
67
- const report = evaluateCatalogue(composed.composed.catalogue, compiled.graph, compiled.profileContext, undefined, composed.composed.catalogues);
67
+ const report = evaluateCatalogue(composed.composed.catalogue, compiled.graph, compiled.profileContext, undefined, composed.composed.catalogues, compiled.patternMemberships);
68
68
  const dismissedEverywhere = new Set(dismissed
69
69
  .filter(({ subject }) => subject === undefined)
70
70
  .map(({ questionId }) => questionId));
@@ -0,0 +1,13 @@
1
+ export interface CoverageScopePattern {
2
+ readonly pattern: string;
3
+ /** Repository-relative files the pattern selected, sorted. */
4
+ readonly artifacts: readonly string[];
5
+ }
6
+ export type ArtifactCoverage = {
7
+ readonly assessed: false;
8
+ readonly reason: string;
9
+ } | {
10
+ readonly assessed: true;
11
+ readonly scope: readonly CoverageScopePattern[];
12
+ };
13
+ export declare function deriveArtifactCoverage(manifestDirectory: string, patterns: readonly string[] | undefined): ArtifactCoverage;
@@ -0,0 +1,58 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { globSync } from 'node:fs';
3
+ import { sep } from 'node:path';
4
+ const runGit = (cwd, args) => spawnSync('git', ['-C', cwd, ...args], { encoding: 'utf8' });
5
+ export function deriveArtifactCoverage(manifestDirectory, patterns) {
6
+ if (patterns === undefined) {
7
+ return {
8
+ assessed: false,
9
+ reason: 'the workspace manifest declares no coverage scope',
10
+ };
11
+ }
12
+ // The root is the git toplevel of the manifest's directory, never the
13
+ // process cwd: the same command must report the same coverage wherever it
14
+ // was invoked (the #216 bug shape), and the repository boundary is git's
15
+ // to draw, not a directory-layout guess.
16
+ const toplevel = runGit(manifestDirectory, [
17
+ 'rev-parse',
18
+ '--show-toplevel',
19
+ ]);
20
+ if (toplevel.status !== 0) {
21
+ return {
22
+ assessed: false,
23
+ reason: 'the workspace does not live in a git repository',
24
+ };
25
+ }
26
+ const root = toplevel.stdout.trim();
27
+ // An artifact is any file git can see: tracked, or untracked and not
28
+ // ignored. Tracked-only would blind the report to exactly the newest
29
+ // files — the recurrence this feature exists to catch (ADR 0130).
30
+ const listed = runGit(root, [
31
+ 'ls-files',
32
+ '-z',
33
+ '--cached',
34
+ '--others',
35
+ '--exclude-standard',
36
+ ]);
37
+ if (listed.status !== 0) {
38
+ return {
39
+ assessed: false,
40
+ reason: `git ls-files failed: ${(listed.stderr ?? '').trim()}`,
41
+ };
42
+ }
43
+ const visible = new Set((listed.stdout ?? '').split('\0').filter((path) => path.length > 0));
44
+ // Glob matches are intersected with git's view, so a symlink escaping the
45
+ // repository or a build tree git ignores cannot enter the artifact set
46
+ // however broad the glob. The intersection also drops directories: git
47
+ // lists files only.
48
+ return {
49
+ assessed: true,
50
+ scope: patterns.map((pattern) => ({
51
+ pattern,
52
+ artifacts: globSync(pattern, { cwd: root })
53
+ .map((path) => path.split(sep).join('/'))
54
+ .filter((path) => visible.has(path))
55
+ .sort((left, right) => left.localeCompare(right)),
56
+ })),
57
+ };
58
+ }
@@ -475,7 +475,7 @@ export function runAskCommand(options, cwd) {
475
475
  }, workspace, cwd), compilation.profileContext);
476
476
  if (!composed.ok)
477
477
  return failed(composed.diagnostics);
478
- const report = evaluateCatalogue(composed.composed.catalogue, compilation.graph, compilation.profileContext, evidenceDocuments.flatMap(({ observations }) => observations), composed.composed.catalogues);
478
+ const report = evaluateCatalogue(composed.composed.catalogue, compilation.graph, compilation.profileContext, evidenceDocuments.flatMap(({ observations }) => observations), composed.composed.catalogues, compilation.patternMemberships);
479
479
  const result = {
480
480
  format: 'yarramate/ask-result/v1',
481
481
  workspace: workspace.id,
@@ -719,7 +719,7 @@ export function runAskCommand(options, cwd) {
719
719
  evidenceObservations.push(...loaded.evidence.observations);
720
720
  }
721
721
  const report = {
722
- ...evaluateCatalogue(composed.composed.catalogue, graph, compilation.profileContext, evidenceObservations, composed.composed.catalogues),
722
+ ...evaluateCatalogue(composed.composed.catalogue, graph, compilation.profileContext, evidenceObservations, composed.composed.catalogues, compilation.patternMemberships),
723
723
  workspace: workspace.id,
724
724
  };
725
725
  // Field by field, to fix key ORDER in the emitted JSON. Every optional
@@ -1021,7 +1021,7 @@ export function runAskCommand(options, cwd) {
1021
1021
  return failed(loaded.diagnostics);
1022
1022
  evidenceDocuments.push(loaded.evidence);
1023
1023
  }
1024
- const report = evaluateCatalogue(composed.composed.catalogue, graph, compilation.profileContext, evidenceDocuments.flatMap(({ observations }) => observations), composed.composed.catalogues);
1024
+ const report = evaluateCatalogue(composed.composed.catalogue, graph, compilation.profileContext, evidenceDocuments.flatMap(({ observations }) => observations), composed.composed.catalogues, compilation.patternMemberships);
1025
1025
  const openQuestions = [];
1026
1026
  for (const wave of report.waves) {
1027
1027
  for (const question of wave.questions) {
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ import { runExportCommand } from './export-command.js';
10
10
  import { runApplyCommand } from './apply-cli.js';
11
11
  import { runDesignCommand } from './design-command.js';
12
12
  import { evaluateEvidenceWorkspace, loadEvidence, } from './evidence.js';
13
+ import { deriveArtifactCoverage } from './artifact-coverage.js';
13
14
  import { deriveAttestationStaleness } from './attestation-staleness.js';
14
15
  import { reconcileEvidenceReports } from './reconciliation.js';
15
16
  import { loadWorkspaceManifest } from './workspace.js';
@@ -84,9 +85,13 @@ const runReconciliation = (options, cwd) => {
84
85
  source: readFileSync(resolve(cwd, path), 'utf8'),
85
86
  documentId: documentIdByPath.get(path) ?? path,
86
87
  })));
88
+ // Coverage anchors on the manifest's own directory, not the process
89
+ // cwd, so the same command reports the same coverage wherever it was
90
+ // invoked (ADR 0130).
91
+ const coverage = deriveArtifactCoverage(dirname(resolve(cwd, workspacePath)), loadedWorkspace.manifest.coverage);
87
92
  return {
88
93
  exitCode: 0,
89
- stdout: `${JSON.stringify(reconcileEvidenceReports(loadedWorkspace.workspace.id, evaluation.reports, compilation.graph, staleness), null, 2)}\n`,
94
+ stdout: `${JSON.stringify(reconcileEvidenceReports(loadedWorkspace.workspace.id, evaluation.reports, compilation.graph, staleness, coverage), null, 2)}\n`,
90
95
  stderr: '',
91
96
  };
92
97
  }
@@ -94,9 +94,31 @@ export interface ResolvedProfileContext {
94
94
  */
95
95
  readonly permittedRelationshipKinds: (fromKindIdentity: string, toKindIdentity: string) => ReadonlySet<RelationshipKind> | undefined;
96
96
  }
97
+ /**
98
+ * One slot of one pattern instance, and the subject bound into it
99
+ * (ADR 0131). Compile CONTEXT, not graph content: `parts` binds existing
100
+ * subjects (#268), the binding is consumed during expansion, and the graph
101
+ * stays indistinguishable from a hand-authored one — so the compile result
102
+ * is the only place this fact survives. `pattern` is the kind identity
103
+ * (`yarrasys/api-led@1.0#api`), the naming ADR 0129 chose: identity that
104
+ * travels, never a document path.
105
+ */
106
+ export interface PatternMembership {
107
+ readonly member: string;
108
+ readonly slot: string;
109
+ readonly instance: string;
110
+ readonly pattern: string;
111
+ }
97
112
  export type CompilationResult = {
98
113
  readonly ok: true;
99
114
  readonly graph: SemanticGraph;
115
+ /**
116
+ * Optional in the type although the compiler always emits it: this
117
+ * shape is published, and a required addition is free for readers
118
+ * and a break for constructors. Read it as `?? []` — and thread it
119
+ * to `evaluateCatalogue`, or `fills-pattern-slot` never fires.
120
+ */
121
+ readonly patternMemberships?: readonly PatternMembership[];
100
122
  } | {
101
123
  readonly ok: false;
102
124
  readonly diagnostics: readonly Diagnostic[];
@@ -105,6 +127,7 @@ export type ContextualCompilationResult = {
105
127
  readonly ok: true;
106
128
  readonly graph: SemanticGraph;
107
129
  readonly profileContext: ResolvedProfileContext;
130
+ readonly patternMemberships?: readonly PatternMembership[];
108
131
  } | {
109
132
  readonly ok: false;
110
133
  readonly diagnostics: readonly Diagnostic[];
package/dist/compiler.js CHANGED
@@ -2332,8 +2332,24 @@ function compileWorkspaceResolved(parsed) {
2332
2332
  if (diagnostics.length > 0) {
2333
2333
  return diagnosticFailure(diagnostics);
2334
2334
  }
2335
+ // Membership survives the compile as context (ADR 0131): one entry per
2336
+ // bound slot, sorted so the emission is deterministic. Always emitted,
2337
+ // possibly empty — an empty array is a workspace with no bindings, while
2338
+ // an evaluation missing the array is a caller that never looked.
2339
+ const patternMemberships = patternInstances
2340
+ .flatMap(({ instance, pattern, bindings }) => [...bindings].map(([slot, member]) => ({
2341
+ member,
2342
+ slot,
2343
+ instance,
2344
+ pattern: pattern.kindIdentity,
2345
+ })))
2346
+ .sort((left, right) => left.member.localeCompare(right.member) ||
2347
+ left.pattern.localeCompare(right.pattern) ||
2348
+ left.instance.localeCompare(right.instance) ||
2349
+ left.slot.localeCompare(right.slot));
2335
2350
  return {
2336
2351
  ok: true,
2352
+ patternMemberships,
2337
2353
  profileContext: {
2338
2354
  conceptKindLineages: immutableMap([...conceptKindByIdentity]
2339
2355
  .sort(([left], [right]) => left.localeCompare(right))
@@ -2418,7 +2434,13 @@ function compileWorkspaceResolved(parsed) {
2418
2434
  }
2419
2435
  export function compileWorkspace(sources) {
2420
2436
  const result = compileWorkspaceResolved(parseSources(sources).parsed);
2421
- return result.ok ? { ok: true, graph: result.graph } : result;
2437
+ return result.ok
2438
+ ? {
2439
+ ok: true,
2440
+ graph: result.graph,
2441
+ patternMemberships: result.patternMemberships,
2442
+ }
2443
+ : result;
2422
2444
  }
2423
2445
  export const compileWorkspaceWithProfileContext = (sources) => compileWorkspaceResolved(parseSources(sources).parsed);
2424
2446
  /**
@@ -246,7 +246,7 @@ export function runDesignCommand(options, cwd) {
246
246
  };
247
247
  }
248
248
  }
249
- const report = evaluateCatalogue(composed.composed.catalogue, compilation.graph, compilation.profileContext, evidenceObservations, composed.composed.catalogues);
249
+ const report = evaluateCatalogue(composed.composed.catalogue, compilation.graph, compilation.profileContext, evidenceObservations, composed.composed.catalogues, compilation.patternMemberships);
250
250
  // Keyed by the QUALIFIED id, matching what the report now carries.
251
251
  const askPlainById = new Map(composed.composed.catalogue.questions.flatMap((question) => question.askPlain === undefined
252
252
  ? []
package/dist/index.d.ts CHANGED
@@ -8,8 +8,9 @@ export { loadWorkspaceManifest, type ResolvedWorkspace, type WorkspaceManifest,
8
8
  export { evaluateEvidence, evaluateEvidenceWorkspace, loadEvidence, type EvidenceDocument, type EvidenceEvaluationResult, type EvidenceLoadResult, type EvidenceLocator, type EvidenceObservation, type EvidenceObservedValue, type EvidenceReport, type EvidenceResult, type EvidenceWorkspaceEvaluationResult, } from './evidence.js';
9
9
  export { constraintExpectsPredicate, reconcileEvidenceReports, type AssertedRelationship, type AttestationStaleness, type DeclaredSource, type EvidenceFinding, type ExpectationComparison, type ReconciliationFinding, type ReconciliationReport, type StaleAttestationFinding, type UnobservedExpectation, } from './reconciliation.js';
10
10
  export { deriveAttestationStaleness } from './attestation-staleness.js';
11
+ export { deriveArtifactCoverage, type ArtifactCoverage, type CoverageScopePattern, } from './artifact-coverage.js';
11
12
  export { buildRtm, renderRtmMarkdown, type RequirementsTraceabilityMatrix, type RtmAttestation, type RtmContextEntry, type RtmDescopedEntry, type RtmEvidenceVerdict, type RtmLineageEntry, type RtmRealizer, type RtmRow, type RtmSource, } from './rtm.js';
12
- export type { CompilationCache, CompilationResult, ContextualCompilationResult, IncrementalCompilationResult, ParsedWorkspaceSource, Diagnostic, GraphClaim, GraphSource, SemanticGraph, ResolvedProfileContext, WorkspaceSource, } from './compiler.js';
13
+ export type { CompilationCache, CompilationResult, ContextualCompilationResult, IncrementalCompilationResult, ParsedWorkspaceSource, PatternMembership, Diagnostic, GraphClaim, GraphSource, SemanticGraph, ResolvedProfileContext, WorkspaceSource, } from './compiler.js';
13
14
  export { canonicalProjection, evaluateProjection, explainProjection, loadProjection, renderProjectionMarkdown, type ConceptFacet, type ProjectionExclusion, } from './projection.js';
14
15
  export { loadAdapterMapping, validateAdapterMapping, validateAdapterMappings, type AdapterMapping, type AdapterMappingLoadResult, type AdapterMappingValidationResult, type AdapterMappingsValidationResult, type AdapterSubjectMapping, } from './adapter-mapping.js';
15
16
  export type { LifecycleStatus, ProjectionDefinition, ProjectionLoadResult, ProjectionResult, } from './projection.js';
@@ -18,4 +19,4 @@ export { applyOperations, landOperations, posixDirectoryOf, type ApplyInput, typ
18
19
  export { connectableKinds, draftRelationship, proposeRelationshipId, stagedSubjectIds, } from './relationship-drafting.js';
19
20
  export { draftConcept, proposeConceptId } from './concept-drafting.js';
20
21
  export { deletionBlockers, describeDeletion, draftDeletion, type DeletionBlocker, } from './deletion-drafting.js';
21
- export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, type CatalogueEvidenceObservation, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
22
+ export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCondition, type CatalogueEvidenceObservation, type CataloguePatternMembership, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export { loadWorkspaceManifest, } from './workspace.js';
8
8
  export { evaluateEvidence, evaluateEvidenceWorkspace, loadEvidence, } from './evidence.js';
9
9
  export { constraintExpectsPredicate, reconcileEvidenceReports, } from './reconciliation.js';
10
10
  export { deriveAttestationStaleness } from './attestation-staleness.js';
11
+ export { deriveArtifactCoverage, } from './artifact-coverage.js';
11
12
  export { buildRtm, renderRtmMarkdown, } from './rtm.js';
12
13
  export { canonicalProjection, evaluateProjection, explainProjection, loadProjection, renderProjectionMarkdown, } from './projection.js';
13
14
  export { loadAdapterMapping, validateAdapterMapping, validateAdapterMappings, } from './adapter-mapping.js';
@@ -89,6 +89,18 @@ export type CatalogueCondition = {
89
89
  readonly condition: 'unchallenged-evidence';
90
90
  } | {
91
91
  readonly condition: 'has-any-subject';
92
+ } | {
93
+ /**
94
+ * The subject fills a slot of a pattern instance (ADR 0131). A GUARD,
95
+ * per the #334 split: it says a question applies here, and an
96
+ * ordinary condition beside it says what would answer it. Bare, it
97
+ * means bound into any slot of any instance; `patternKinds` narrows
98
+ * by the pattern's kind identity (never a document path, ADR 0129)
99
+ * and `slots` by part name.
100
+ */
101
+ readonly condition: 'fills-pattern-slot';
102
+ readonly patternKinds?: readonly string[];
103
+ readonly slots?: readonly string[];
92
104
  };
93
105
  /**
94
106
  * One observation from the workspace's evidence overlay, reduced to what
@@ -107,6 +119,21 @@ export interface CatalogueEvidenceObservation {
107
119
  readonly result: 'confirmed' | 'contradicted' | 'unknown' | 'not-observed';
108
120
  readonly searched?: readonly unknown[];
109
121
  }
122
+ /**
123
+ * One slot of one pattern instance and the subject bound into it, as
124
+ * interrogation reads it. Shaped structurally rather than importing the
125
+ * compiler's {@link PatternMembership} for the same reason
126
+ * {@link CatalogueEvidenceObservation} is: the pure engine entry keeps
127
+ * owning its whole input surface, and a host passes
128
+ * `compilation.patternMemberships` without the compiler's types. Only
129
+ * `fills-pattern-slot` reads it (ADR 0131).
130
+ */
131
+ export interface CataloguePatternMembership {
132
+ readonly member: string;
133
+ readonly slot: string;
134
+ readonly instance: string;
135
+ readonly pattern: string;
136
+ }
110
137
  export interface CatalogueQuestion {
111
138
  readonly id: string;
112
139
  readonly wave: string;
@@ -210,7 +237,15 @@ export declare function evaluateCatalogue(catalogue: QuestionCatalogue, graph: S
210
237
  * the report. A fifth optional parameter rather than an options object,
211
238
  * because this signature is published and a consumer already calls it.
212
239
  */
213
- catalogues?: readonly string[]): Omit<InterrogationReport, 'workspace'>;
240
+ catalogues?: readonly string[],
241
+ /**
242
+ * Pattern memberships from the compilation (ADR 0131) — pass
243
+ * `compilation.patternMemberships`, or `fills-pattern-slot` conditions
244
+ * never fire. A sixth optional parameter for the same reason
245
+ * `catalogues` is a fifth: this signature is published and a consumer
246
+ * already calls it.
247
+ */
248
+ patternMemberships?: readonly CataloguePatternMembership[]): Omit<InterrogationReport, 'workspace'>;
214
249
  export type CatalogueLoadResult = {
215
250
  readonly ok: true;
216
251
  readonly catalogue: QuestionCatalogue;
@@ -236,8 +236,20 @@ const linkageHits = (index, condition, subjectId, profileContext) => {
236
236
  return counterparts.some((counterpart) => kindMatches(index.kindOf.get(counterpart), condition.counterpartKinds, matching, profileContext));
237
237
  });
238
238
  };
239
- const conditionHolds = (index, condition, subjectId, profileContext, evidence) => {
239
+ const conditionHolds = (index, condition, subjectId, profileContext, evidence, memberships) => {
240
240
  switch (condition.condition) {
241
+ case 'fills-pattern-slot':
242
+ // Absent memberships stay quiet: the caller did not derive them, so
243
+ // participation is unknown, not absent — the same rule
244
+ // `unchallenged-evidence` applies to a missing overlay and
245
+ // `unconstrained-kind` to a missing profile context (ADR 0131).
246
+ return (memberships !== undefined &&
247
+ subjectId !== undefined &&
248
+ memberships.some((membership) => membership.member === subjectId &&
249
+ (condition.patternKinds === undefined ||
250
+ condition.patternKinds.includes(membership.pattern)) &&
251
+ (condition.slots === undefined ||
252
+ condition.slots.includes(membership.slot))));
241
253
  case 'has-any-subject':
242
254
  // The guard a late wave needs to say "only once the model has
243
255
  // substance" (#334). An empty model is not an architecture at rest -
@@ -459,13 +471,21 @@ export function evaluateCatalogue(catalogue, graph, profileContext, evidence,
459
471
  * the report. A fifth optional parameter rather than an options object,
460
472
  * because this signature is published and a consumer already calls it.
461
473
  */
462
- catalogues) {
474
+ catalogues,
475
+ /**
476
+ * Pattern memberships from the compilation (ADR 0131) — pass
477
+ * `compilation.patternMemberships`, or `fills-pattern-slot` conditions
478
+ * never fire. A sixth optional parameter for the same reason
479
+ * `catalogues` is a fifth: this signature is published and a consumer
480
+ * already calls it.
481
+ */
482
+ patternMemberships) {
463
483
  const index = indexGraph(graph);
464
484
  let open = 0;
465
485
  let openQuestions = 0;
466
486
  const applicableQuestions = catalogue.questions.filter((question) => questionIsApplicable(question, graph.profiles));
467
487
  const waveOpens = (wave) => wave.opensWhen === undefined ||
468
- wave.opensWhen.every((condition) => conditionHolds(index, condition, undefined, profileContext, evidence));
488
+ wave.opensWhen.every((condition) => conditionHolds(index, condition, undefined, profileContext, evidence, patternMemberships));
469
489
  const waves = catalogue.waves.map((wave) => ({
470
490
  id: wave.id,
471
491
  name: wave.name,
@@ -490,14 +510,14 @@ catalogues) {
490
510
  ...(question.since === undefined ? {} : { since: question.since }),
491
511
  };
492
512
  if (question.scope === 'workspace') {
493
- const isOpen = question.trigger.every((condition) => conditionHolds(index, condition, undefined, profileContext, evidence));
513
+ const isOpen = question.trigger.every((condition) => conditionHolds(index, condition, undefined, profileContext, evidence, patternMemberships));
494
514
  if (isOpen) {
495
515
  open += 1;
496
516
  openQuestions += 1;
497
517
  }
498
518
  return { ...base, open: isOpen };
499
519
  }
500
- const matches = selectSubjects(index, question.subjects, profileContext).filter((id) => question.trigger.every((condition) => conditionHolds(index, condition, id, profileContext, evidence)));
520
+ const matches = selectSubjects(index, question.subjects, profileContext).filter((id) => question.trigger.every((condition) => conditionHolds(index, condition, id, profileContext, evidence, patternMemberships)));
501
521
  if (matches.length === 0) {
502
522
  return { ...base, open: false };
503
523
  }
@@ -556,7 +576,7 @@ const kindReferencesOf = (catalogue) => {
556
576
  const fromCondition = (condition, path) => {
557
577
  if (typeof condition !== 'object' || condition === null)
558
578
  return;
559
- for (const field of ['kinds', 'counterpartKinds']) {
579
+ for (const field of ['kinds', 'counterpartKinds', 'patternKinds']) {
560
580
  const value = condition[field];
561
581
  if (!Array.isArray(value))
562
582
  continue;
@@ -1 +1 @@
1
- export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCompositionResult, type ComposedCatalogue, type CatalogueCondition, type CatalogueEvidenceObservation, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
1
+ export { INTERROGATION_SEMANTICS_VERSION, composeCatalogues, qualifiedQuestionId, evaluateCatalogue, loadQuestionCatalogue, renderInterrogationReport, renderQuestion, type CatalogueCompositionResult, type ComposedCatalogue, type CatalogueCondition, type CatalogueEvidenceObservation, type CataloguePatternMembership, type CatalogueLoadResult, type CatalogueQuestion, type CatalogueSelector, type InterrogationReport, type InterrogationSummary, type OpenSubject, type QuestionCatalogue, type ReportQuestion, type ReportWave, } from './interrogate-command.js';
@@ -1,5 +1,6 @@
1
1
  import type { SemanticGraph } from './compiler.js';
2
2
  import type { EvidenceLocator, EvidenceReport, EvidenceResult } from './evidence.js';
3
+ import type { ArtifactCoverage } from './artifact-coverage.js';
3
4
  export interface AssertedRelationship {
4
5
  readonly from: string;
5
6
  readonly to: string;
@@ -98,11 +99,30 @@ export interface ReconciliationReport {
98
99
  readonly unconfirmedAttestations?: number;
99
100
  readonly expectationsCompared: number;
100
101
  readonly expectationsWithoutObservation: number;
102
+ /**
103
+ * Files the declared coverage scope selected, and the ones no
104
+ * observation's `repo:` locator claims (ADR 0130). Both appear exactly
105
+ * when coverage was assessed, so a report without them is one that
106
+ * never looked, not one that found nothing.
107
+ */
108
+ readonly artifactsInScope?: number;
109
+ readonly unclaimedArtifacts?: number;
101
110
  };
102
111
  readonly findings: readonly ReconciliationFinding[];
103
112
  readonly unobservedSubjects?: readonly string[];
104
113
  readonly unobservedExpectations?: readonly UnobservedExpectation[];
114
+ /**
115
+ * The coverage patterns as the manifest declared them, echoed so the
116
+ * report is honest about what it was asked to look at (ADR 0130).
117
+ */
118
+ readonly coverageScope?: readonly string[];
119
+ /**
120
+ * In-scope artifacts no observation claims, sorted. Absence, never
121
+ * accusation: no finding is fabricated and `check --strict` never reads
122
+ * this (ADR 0130), the line ADR 0049 drew for unobserved subjects.
123
+ */
124
+ readonly unclaimedArtifacts?: readonly string[];
105
125
  readonly notes?: readonly string[];
106
126
  }
107
127
  export declare const constraintExpectsPredicate = "yarramate/constraint/expects";
108
- export declare function reconcileEvidenceReports(workspace: string, reports: readonly EvidenceReport[], graph?: SemanticGraph, staleness?: AttestationStaleness): ReconciliationReport;
128
+ export declare function reconcileEvidenceReports(workspace: string, reports: readonly EvidenceReport[], graph?: SemanticGraph, staleness?: AttestationStaleness, coverage?: ArtifactCoverage): ReconciliationReport;
@@ -149,6 +149,76 @@ const unobservedCurrentConcepts = (graph, reports) => {
149
149
  .map(({ subject }) => subject)
150
150
  .sort((left, right) => left.localeCompare(right));
151
151
  };
152
+ // An observation claims an artifact when its locator is `repo:<path>`,
153
+ // read syntactically: any `#fragment` stripped, a directory claiming
154
+ // everything beneath it, any other scheme claiming nothing. Resolution
155
+ // and external validity stay with the provider (ADR 0130) — nothing here
156
+ // opens a file or checks one exists.
157
+ const claimedArtifactPaths = (reports) => {
158
+ const claimed = new Set();
159
+ for (const report of reports) {
160
+ for (const observation of report.observations) {
161
+ const uri = observation.evidence.uri;
162
+ if (!uri.startsWith('repo:'))
163
+ continue;
164
+ const located = uri.slice('repo:'.length);
165
+ const fragment = located.indexOf('#');
166
+ // Trailing slashes are stripped with a loop, not a regex: an
167
+ // anchored /\/+$/ is polynomial on a locator of many slashes, and
168
+ // locators are library input.
169
+ let path = fragment === -1 ? located : located.slice(0, fragment);
170
+ while (path.endsWith('/'))
171
+ path = path.slice(0, -1);
172
+ if (path.length > 0)
173
+ claimed.add(path);
174
+ }
175
+ }
176
+ return claimed;
177
+ };
178
+ const isClaimed = (claimed, artifact) => {
179
+ if (claimed.has(artifact))
180
+ return true;
181
+ for (const path of claimed) {
182
+ if (artifact.startsWith(`${path}/`))
183
+ return true;
184
+ }
185
+ return false;
186
+ };
187
+ const assessArtifactCoverage = (coverage, reports) => {
188
+ // An absent argument is a caller that never looked (the strict gate);
189
+ // an unassessed derivation is a reconcile that looked and could not,
190
+ // and says why (ADR 0130, the ADR 0074 parallel).
191
+ if (coverage === undefined)
192
+ return { notes: [] };
193
+ if (!coverage.assessed) {
194
+ return {
195
+ notes: [`Artifact coverage was not assessed: ${coverage.reason}.`],
196
+ };
197
+ }
198
+ const notes = [];
199
+ if (coverage.scope.length === 0) {
200
+ notes.push('The workspace manifest declares an empty coverage scope, so no artifacts were assessed.');
201
+ }
202
+ // A dead glob is indistinguishable from a typo, and silently
203
+ // contributing nothing is how a mistyped pattern would report full
204
+ // coverage. A note, not a refusal: a coverage scope is a lens, not
205
+ // load-bearing input (ADR 0130).
206
+ for (const { pattern, artifacts } of coverage.scope) {
207
+ if (artifacts.length === 0) {
208
+ notes.push(`Coverage pattern "${pattern}" matched no artifacts.`);
209
+ }
210
+ }
211
+ const artifacts = [
212
+ ...new Set(coverage.scope.flatMap(({ artifacts }) => artifacts)),
213
+ ].sort((left, right) => left.localeCompare(right));
214
+ const claimed = claimedArtifactPaths(reports);
215
+ return {
216
+ scope: coverage.scope.map(({ pattern }) => pattern),
217
+ artifacts: artifacts.length,
218
+ unclaimed: artifacts.filter((artifact) => !isClaimed(claimed, artifact)),
219
+ notes,
220
+ };
221
+ };
152
222
  // A judgment a machine transcribed is not the act the authority
153
223
  // performed. The recorder is in the model, so the difference is
154
224
  // derivable here: an authority who wrote the record in their own hand
@@ -186,11 +256,12 @@ const unconfirmedAttestations = (graph) => {
186
256
  ];
187
257
  });
188
258
  };
189
- export function reconcileEvidenceReports(workspace, reports, graph, staleness) {
259
+ export function reconcileEvidenceReports(workspace, reports, graph, staleness, coverage) {
190
260
  const assertedByClaim = assertedRelationshipsByClaim(graph);
191
261
  const unobservedSubjects = unobservedCurrentConcepts(graph, reports);
192
262
  const expectations = compareExpectations(graph, reports);
193
263
  const unconfirmed = unconfirmedAttestations(graph);
264
+ const assessedCoverage = assessArtifactCoverage(coverage, reports);
194
265
  const summary = {
195
266
  evidenceDocuments: reports.length,
196
267
  observations: 0,
@@ -215,6 +286,15 @@ export function reconcileEvidenceReports(workspace, reports, graph, staleness) {
215
286
  : { unconfirmedAttestations: unconfirmed.length }),
216
287
  expectationsCompared: expectations.compared,
217
288
  expectationsWithoutObservation: expectations.unobserved.length,
289
+ // Coverage counters appear exactly when the scope was assessed
290
+ // (ADR 0130): a report without them never looked; a report carrying
291
+ // zero looked and found everything claimed.
292
+ ...(assessedCoverage.artifacts === undefined
293
+ ? {}
294
+ : {
295
+ artifactsInScope: assessedCoverage.artifacts,
296
+ unclaimedArtifacts: assessedCoverage.unclaimed?.length ?? 0,
297
+ }),
218
298
  };
219
299
  const findings = [
220
300
  ...(staleness?.findings ?? []),
@@ -274,7 +354,11 @@ export function reconcileEvidenceReports(workspace, reports, graph, staleness) {
274
354
  (expectationOf(left)?.key ?? '').localeCompare(expectationOf(right)?.key ?? '') ||
275
355
  (expectationOf(left)?.observed ?? '').localeCompare(expectationOf(right)?.observed ?? ''));
276
356
  summary.findings = findings.length;
277
- const notes = [...(staleness?.notes ?? []), ...absenceNotes];
357
+ const notes = [
358
+ ...(staleness?.notes ?? []),
359
+ ...absenceNotes,
360
+ ...assessedCoverage.notes,
361
+ ];
278
362
  return {
279
363
  format: 'yarramate/reconciliation-report/v1',
280
364
  workspace,
@@ -284,6 +368,13 @@ export function reconcileEvidenceReports(workspace, reports, graph, staleness) {
284
368
  ...(expectations.unobserved.length === 0
285
369
  ? {}
286
370
  : { unobservedExpectations: expectations.unobserved }),
371
+ ...(assessedCoverage.scope === undefined
372
+ ? {}
373
+ : { coverageScope: assessedCoverage.scope }),
374
+ ...(assessedCoverage.unclaimed === undefined ||
375
+ assessedCoverage.unclaimed.length === 0
376
+ ? {}
377
+ : { unclaimedArtifacts: assessedCoverage.unclaimed }),
287
378
  ...(notes.length === 0 ? {} : { notes }),
288
379
  };
289
380
  }