yarramate 1.27.0 → 1.29.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.
@@ -37,6 +37,20 @@ export interface VisualQuestionEntry {
37
37
  */
38
38
  readonly trigger?: readonly CatalogueCondition[];
39
39
  }
40
+ /**
41
+ * The next load-bearing open question (#534, ADR 0154): the row `design`
42
+ * would serve first, by design's own rule (the first open question in wave
43
+ * order, then catalogue order within the wave; a subject-scoped question
44
+ * names the first open subject it is open for). Computed host-side beside
45
+ * the overlay so both hosts and the browser agree on which row it is, and
46
+ * nobody re-derives the rule from the lists.
47
+ */
48
+ export interface VisualNextQuestion extends VisualQuestionEntry {
49
+ readonly wave: string;
50
+ /** The subject it is open for; absent for a workspace-scoped question. */
51
+ readonly subjectId?: string;
52
+ readonly subjectName?: string;
53
+ }
40
54
  /**
41
55
  * The interrogation report, folded for drawing (#292).
42
56
  *
@@ -56,6 +70,12 @@ export interface VisualInterrogationOverlay {
56
70
  readonly workspace: readonly VisualQuestionEntry[];
57
71
  /** Open questions per qualified subject id — `CanvasNode.id`'s space. */
58
72
  readonly subjects: Readonly<Record<string, readonly VisualQuestionEntry[]>>;
73
+ /**
74
+ * The one to ask next (#534). Absent when nothing is open, or when the
75
+ * host that built this overlay predates the field: a reader treats absence
76
+ * as "no next question", never as a fault.
77
+ */
78
+ readonly next?: VisualNextQuestion;
59
79
  }
60
80
  /** The resolved graph a session renders, as the browser receives it. */
61
81
  export interface VisualRenderedModel {
@@ -132,6 +132,10 @@ dismissed = []) => {
132
132
  .map(({ questionId, subject }) => `${questionId}\u0000${subject}`));
133
133
  const workspace = [];
134
134
  const subjects = {};
135
+ // The first row kept is the next question (#534): the loop already walks
136
+ // waves in order and questions in catalogue order, which is `design`'s
137
+ // rule, so "first kept" is the rule and not a second one.
138
+ let next;
135
139
  for (const wave of report.waves) {
136
140
  for (const question of wave.questions) {
137
141
  if (!question.open)
@@ -149,22 +153,31 @@ dismissed = []) => {
149
153
  trigger: question.trigger,
150
154
  };
151
155
  if (question.subjects === undefined) {
152
- workspace.push({
156
+ const row = {
153
157
  ...base,
154
158
  scope: "workspace",
155
159
  question: question.question,
156
- });
160
+ };
161
+ workspace.push(row);
162
+ next ??= { ...row, wave: wave.id };
157
163
  continue;
158
164
  }
159
165
  for (const subject of question.subjects) {
160
166
  if (dismissedForSubject.has(`${question.id}\u0000${subject.id}`)) {
161
167
  continue;
162
168
  }
163
- (subjects[subject.id] ??= []).push({
169
+ const row = {
164
170
  ...base,
165
171
  scope: "subject",
166
172
  question: subject.question,
167
- });
173
+ };
174
+ (subjects[subject.id] ??= []).push(row);
175
+ next ??= {
176
+ ...row,
177
+ wave: wave.id,
178
+ subjectId: subject.id,
179
+ ...(subject.name === undefined ? {} : { subjectName: subject.name }),
180
+ };
168
181
  }
169
182
  }
170
183
  }
@@ -173,6 +186,7 @@ dismissed = []) => {
173
186
  semantics: report.semantics,
174
187
  workspace,
175
188
  subjects,
189
+ ...(next === undefined ? {} : { next }),
176
190
  };
177
191
  };
178
192
  /**
@@ -7,7 +7,7 @@ export interface CliResult {
7
7
  export declare const isMainModule: (moduleUrl: string, entrypoint: string | undefined) => boolean;
8
8
  export declare const packageVersion: string;
9
9
  export declare const versionResult: (binary: string) => CliResult;
10
- export declare const usage = "Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <subject-id>] [--catalogue <catalogue.yaml>] [--facilitate] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> \"<free text>\" | <subject-id> ... | <projection.yaml> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise \"<topic>\" [--budget <tokens>] [--neighbours <n>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --where \"<free text>\" | <subject-id> ... [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml> [--json]\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export rtm <workspace.yaml> --out <directory>\n yarramate export xlsx <projection.yaml> <workspace.yaml> --out <file>\n yarramate import xlsx <workbook.xlsx> <workspace.yaml> [--json]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml> [--changed <git-range>]\n";
10
+ export declare const usage = "Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <subject-id>] [--catalogue <catalogue.yaml>] [--facilitate] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> \"<free text>\" | <subject-id> ... | <projection.yaml> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise \"<topic>\" [--budget <tokens>] [--neighbours <n>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --where \"<free text>\" | <subject-id> ... [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml> [--json | --text]\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export rtm <workspace.yaml> --out <directory>\n yarramate export xlsx <projection.yaml> <workspace.yaml> --out <file>\n yarramate import xlsx <workbook.xlsx> <workspace.yaml> [--json]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml> [--changed <git-range>]\n";
11
11
  export declare const diagnosticJson: (diagnostics: unknown) => string;
12
12
  export declare const checkResultJson: (ok: boolean, diagnostics: unknown, counted?: {
13
13
  readonly documents: number;
@@ -22,7 +22,7 @@ export const versionResult = (binary) => ({
22
22
  stdout: `${binary} ${packageVersion}\n`,
23
23
  stderr: '',
24
24
  });
25
- export const usage = 'Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <subject-id>] [--catalogue <catalogue.yaml>] [--facilitate] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> "<free text>" | <subject-id> ... | <projection.yaml> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise "<topic>" [--budget <tokens>] [--neighbours <n>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --where "<free text>" | <subject-id> ... [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml> [--json]\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export rtm <workspace.yaml> --out <directory>\n yarramate export xlsx <projection.yaml> <workspace.yaml> --out <file>\n yarramate import xlsx <workbook.xlsx> <workspace.yaml> [--json]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml> [--changed <git-range>]\n';
25
+ export const usage = 'Usage:\n yarramate init <directory> [--no-pointer]\n yarramate design <workspace.yaml> [--subject <subject-id>] [--catalogue <catalogue.yaml>] [--facilitate] [--json]\n yarramate apply <operations.yaml> <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> [--json]\n yarramate ask <workspace.yaml> "<free text>" | <subject-id> ... | <projection.yaml> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate ask <workspace.yaml> --subjects [--kind <term>] [--status <status>] [--json]\n yarramate ask <workspace.yaml> --kinds [--json]\n yarramate ask <workspace.yaml> --advise "<topic>" [--budget <tokens>] [--neighbours <n>] [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --where "<free text>" | <subject-id> ... [--json]\n yarramate ask <workspace.yaml> --next [--json]\n yarramate ask <workspace.yaml> --open [--catalogue <catalogue.yaml>] [--json]\n yarramate ask <workspace.yaml> --compare <from-state> <to-state> [--json]\n yarramate ask <workspace.yaml> --changed <git-range> [--budget <tokens>] [--neighbours <n>] [--json]\n yarramate check <source.yaml> [source.yaml ...] [--json] [--strict]\n yarramate reconcile <workspace.yaml> [--json | --text]\n yarramate export graph <workspace.yaml> [--out <file>]\n yarramate export markdown <projection.yaml> <workspace.yaml> [--out <file>]\n yarramate export markdown --changed <git-range> <workspace.yaml> [--out <file>]\n yarramate export briefs <projection.yaml> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export briefs --changed <git-range> <workspace.yaml> --out <directory> [--budget <tokens>]\n yarramate export rtm <workspace.yaml> --out <directory>\n yarramate export xlsx <projection.yaml> <workspace.yaml> --out <file>\n yarramate import xlsx <workbook.xlsx> <workspace.yaml> [--json]\n yarramate export likec4 <likec4-project.yaml> <output-dir> <workspace.yaml> [--changed <git-range>]\n';
26
26
  export const diagnosticJson = (diagnostics) => `${JSON.stringify({
27
27
  format: 'yarramate/diagnostic-result/v1',
28
28
  diagnostics,
package/dist/cli.js CHANGED
@@ -13,16 +13,22 @@ import { evaluateEvidenceWorkspace, loadEvidence, } from './evidence.js';
13
13
  import { deriveArtifactCoverage } from './artifact-coverage.js';
14
14
  import { deriveAttestationStaleness } from './attestation-staleness.js';
15
15
  import { reconcileEvidenceReports } from './reconciliation.js';
16
+ import { reconciliationReportText } from './reconciliation-text.js';
16
17
  import { loadWorkspaceManifest } from './workspace.js';
17
18
  const runReconciliation = (options, cwd) => {
18
19
  // Bare reconcile already emits JSON, so --json changes nothing — but a
19
20
  // harness scripting "add --json to every verb" must not hit exit 2 on
20
21
  // the one verb that treats it as unknown (#275). Accepted as a no-op.
21
- const positional = options.filter((option) => option !== '--json');
22
+ // --text is the same report for a person (#526, ADR 0153); asking for
23
+ // both shapes at once names no output, so it is refused like any other
24
+ // malformed call.
25
+ const text = options.includes('--text');
26
+ const positional = options.filter((option) => option !== '--json' && option !== '--text');
22
27
  const [workspacePath] = positional;
23
28
  if (positional.length !== 1 ||
24
29
  workspacePath === undefined ||
25
- workspacePath.startsWith('-')) {
30
+ workspacePath.startsWith('-') ||
31
+ (text && options.includes('--json'))) {
26
32
  return { exitCode: 2, stdout: '', stderr: usage };
27
33
  }
28
34
  try {
@@ -89,9 +95,12 @@ const runReconciliation = (options, cwd) => {
89
95
  // cwd, so the same command reports the same coverage wherever it was
90
96
  // invoked (ADR 0130).
91
97
  const coverage = deriveArtifactCoverage(dirname(resolve(cwd, workspacePath)), loadedWorkspace.manifest.coverage);
98
+ const report = reconcileEvidenceReports(loadedWorkspace.workspace.id, evaluation.reports, compilation.graph, staleness, coverage);
92
99
  return {
93
100
  exitCode: 0,
94
- stdout: `${JSON.stringify(reconcileEvidenceReports(loadedWorkspace.workspace.id, evaluation.reports, compilation.graph, staleness, coverage), null, 2)}\n`,
101
+ stdout: text
102
+ ? reconciliationReportText(report)
103
+ : `${JSON.stringify(report, null, 2)}\n`,
95
104
  stderr: '',
96
105
  };
97
106
  }
@@ -0,0 +1,13 @@
1
+ import type { ReconciliationReport } from './reconciliation.js';
2
+ /**
3
+ * The reconciliation report for a person (#526, ADR 0153).
4
+ *
5
+ * The JSON report is the contract (EVIDENCE.md) and stays the verb's
6
+ * default; this is the same report said in lines, in the same order, with
7
+ * nothing added and nothing judged. Every count the summary carries is on
8
+ * the first lines; every finding is one line, plus its message where the
9
+ * provider left one; the lists that follow appear only when they hold
10
+ * something, and the notes close, because they are what the report says
11
+ * about its own limits.
12
+ */
13
+ export declare const reconciliationReportText: (report: ReconciliationReport) => string;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The reconciliation report for a person (#526, ADR 0153).
3
+ *
4
+ * The JSON report is the contract (EVIDENCE.md) and stays the verb's
5
+ * default; this is the same report said in lines, in the same order, with
6
+ * nothing added and nothing judged. Every count the summary carries is on
7
+ * the first lines; every finding is one line, plus its message where the
8
+ * provider left one; the lists that follow appear only when they hold
9
+ * something, and the notes close, because they are what the report says
10
+ * about its own limits.
11
+ */
12
+ export const reconciliationReportText = (report) => {
13
+ const { summary } = report;
14
+ const lines = [];
15
+ const row = (label, ...cells) => {
16
+ lines.push(`${label.padEnd(16)}${cells.join(' ')}`);
17
+ };
18
+ row('reconciliation', report.workspace);
19
+ row('observations', String(summary.observations), `confirmed ${summary.confirmed}`, `contradicted ${summary.contradicted}`, `unknown ${summary.unknown}`, `not observed ${summary.notObserved}`, ...(summary.unsupportedAbsences === undefined
20
+ ? []
21
+ : [`unsupported absences ${summary.unsupportedAbsences}`]));
22
+ if (summary.staleAttestations !== undefined ||
23
+ summary.unconfirmedAttestations !== undefined) {
24
+ row('attestations', `stale ${summary.staleAttestations ?? 0}`, `unconfirmed ${summary.unconfirmedAttestations ?? 0}`);
25
+ }
26
+ row('expectations', `compared ${summary.expectationsCompared}`, `without observation ${summary.expectationsWithoutObservation}`);
27
+ row('subjects', `without evidence ${summary.subjectsWithoutEvidence}`);
28
+ row('artifacts', ...(summary.artifactsInScope === undefined
29
+ ? ['not assessed']
30
+ : [
31
+ `in scope ${summary.artifactsInScope}`,
32
+ `unclaimed ${summary.unclaimedArtifacts ?? 0}`,
33
+ ]));
34
+ row('findings', String(summary.findings));
35
+ const section = (title, body) => {
36
+ if (body.length === 0)
37
+ return;
38
+ lines.push('', title);
39
+ lines.push(...body);
40
+ };
41
+ section('Findings', report.findings.flatMap((finding) => findingLines(finding)));
42
+ section('Subjects without evidence', (report.unobservedSubjects ?? []).map((id) => ` ${id}`));
43
+ section('Expectations without observation', (report.unobservedExpectations ?? []).map((expectation) => ` ${expectation.subject} ${expectation.provider}/${expectation.key} expected ${expectation.expected} ${expectation.declared.path}:${expectation.declared.line}`));
44
+ section('Unclaimed artifacts', (report.unclaimedArtifacts ?? []).map((path) => ` ${path}`));
45
+ section('Coverage scope', (report.coverageScope ?? []).map((pattern) => ` ${pattern}`));
46
+ section('Notes', (report.notes ?? []).map((note) => ` ${note}`));
47
+ return `${lines.join('\n')}\n`;
48
+ };
49
+ const findingLines = (finding) => {
50
+ const label = finding.result.replace(/-/g, ' ').padEnd(24);
51
+ if (finding.result === 'stale-attestation') {
52
+ const changed = finding.changedAt === undefined
53
+ ? ''
54
+ : `; the subject's wording changed ${finding.changedAt}`;
55
+ return [
56
+ ` ${label}${finding.target.id} ${finding.attestation.topic}, by ${finding.attestation.by} on ${finding.attestation.on}${changed} ${finding.evidence.uri}`,
57
+ ];
58
+ }
59
+ if (finding.result === 'unconfirmed-attestation') {
60
+ return [
61
+ ` ${label}${finding.target.id} ${finding.attestation.topic}, by ${finding.attestation.by}, recorded by ${finding.attestation.recordedBy} on ${finding.attestation.on} ${finding.declared.path}:${finding.declared.line}`,
62
+ ];
63
+ }
64
+ const head = ` ${label}${finding.target.type === 'claim' ? 'claim ' : ''}${finding.target.id} ${finding.provider}, ${finding.evidenceDocument} ${finding.evidence.uri}`;
65
+ const detail = [];
66
+ if (finding.asserted !== undefined) {
67
+ detail.push(` asserts ${finding.asserted.from} -${finding.asserted.kind}-> ${finding.asserted.to}`);
68
+ }
69
+ if (finding.expectation !== undefined) {
70
+ detail.push(` expected ${finding.expectation.key} = ${finding.expectation.expected}, observed ${finding.expectation.observed} ${finding.expectation.declared.path}:${finding.expectation.declared.line}`);
71
+ }
72
+ if (finding.evidence.message !== undefined) {
73
+ detail.push(` ${finding.evidence.message}`);
74
+ }
75
+ return [head, ...detail];
76
+ };