@sentropic/track 0.87.0 → 0.89.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 (80) hide show
  1. package/dist/cli/index.d.ts.map +1 -1
  2. package/dist/cli/index.js +209 -20
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/events/types.d.ts +1 -1
  5. package/dist/events/types.d.ts.map +1 -1
  6. package/dist/events/types.js +16 -0
  7. package/dist/events/types.js.map +1 -1
  8. package/dist/events/validate.d.ts +10 -0
  9. package/dist/events/validate.d.ts.map +1 -1
  10. package/dist/events/validate.js +81 -0
  11. package/dist/events/validate.js.map +1 -1
  12. package/dist/ingest/contract.d.ts +3 -2
  13. package/dist/ingest/contract.d.ts.map +1 -1
  14. package/dist/ingest/contract.js +33 -1
  15. package/dist/ingest/contract.js.map +1 -1
  16. package/dist/ingest/index.d.ts.map +1 -1
  17. package/dist/ingest/index.js +6 -5
  18. package/dist/ingest/index.js.map +1 -1
  19. package/dist/ingest/ingest.d.ts.map +1 -1
  20. package/dist/ingest/ingest.js +17 -0
  21. package/dist/ingest/ingest.js.map +1 -1
  22. package/dist/ingest/map.d.ts.map +1 -1
  23. package/dist/ingest/map.js +11 -0
  24. package/dist/ingest/map.js.map +1 -1
  25. package/dist/model/index.d.ts +1 -1
  26. package/dist/model/index.d.ts.map +1 -1
  27. package/dist/model/index.js +1 -1
  28. package/dist/model/index.js.map +1 -1
  29. package/dist/model/item.d.ts +65 -0
  30. package/dist/model/item.d.ts.map +1 -1
  31. package/dist/model/item.js +49 -0
  32. package/dist/model/item.js.map +1 -1
  33. package/dist/read/commands.d.ts +24 -18
  34. package/dist/read/commands.d.ts.map +1 -1
  35. package/dist/read/commands.js +167 -26
  36. package/dist/read/commands.js.map +1 -1
  37. package/dist/read/contract.d.ts +32 -3
  38. package/dist/read/contract.d.ts.map +1 -1
  39. package/dist/read/contract.js +42 -2
  40. package/dist/read/contract.js.map +1 -1
  41. package/dist/report/blocker-status.d.ts.map +1 -1
  42. package/dist/report/blocker-status.js +11 -0
  43. package/dist/report/blocker-status.js.map +1 -1
  44. package/dist/report/build.d.ts +6 -0
  45. package/dist/report/build.d.ts.map +1 -1
  46. package/dist/report/build.js +2 -31
  47. package/dist/report/build.js.map +1 -1
  48. package/dist/report/format.d.ts +227 -8
  49. package/dist/report/format.d.ts.map +1 -1
  50. package/dist/report/format.js +946 -106
  51. package/dist/report/format.js.map +1 -1
  52. package/dist/report/index.d.ts +1 -1
  53. package/dist/report/index.d.ts.map +1 -1
  54. package/dist/report/index.js +1 -1
  55. package/dist/report/index.js.map +1 -1
  56. package/dist/report/rollup.d.ts +14 -0
  57. package/dist/report/rollup.d.ts.map +1 -1
  58. package/dist/report/rollup.js +32 -0
  59. package/dist/report/rollup.js.map +1 -1
  60. package/dist/report/snapshot.d.ts +20 -1
  61. package/dist/report/snapshot.d.ts.map +1 -1
  62. package/dist/report/snapshot.js +9 -2
  63. package/dist/report/snapshot.js.map +1 -1
  64. package/dist/state/fold.d.ts.map +1 -1
  65. package/dist/state/fold.js +48 -0
  66. package/dist/state/fold.js.map +1 -1
  67. package/dist/track.d.ts +34 -1
  68. package/dist/track.d.ts.map +1 -1
  69. package/dist/track.js +95 -2
  70. package/dist/track.js.map +1 -1
  71. package/package.json +1 -1
  72. package/skills/track-operation/SKILL.md +16 -4
  73. package/dist/report/html.d.ts +0 -9
  74. package/dist/report/html.d.ts.map +0 -1
  75. package/dist/report/html.js +0 -76
  76. package/dist/report/html.js.map +0 -1
  77. package/dist/report/present.d.ts +0 -28
  78. package/dist/report/present.d.ts.map +0 -1
  79. package/dist/report/present.js +0 -32
  80. package/dist/report/present.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AA6CA,MAAM,WAAW,KAAK;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACxB,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACxB,kGAAkG;IAClG,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CACxB;AAuSD,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsJ7E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AA8CA,MAAM,WAAW,KAAK;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACxB,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACxB,kGAAkG;IAClG,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CACxB;AAqZD,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA8J7E"}
package/dist/cli/index.js CHANGED
@@ -11,26 +11,31 @@ import { initTrackDir, resolveTrackDir, resolveTrackDirOrNull } from './resolve.
11
11
  import { DomainError } from '../model/item.js';
12
12
  import { displayText, formatRows } from '../report/format.js';
13
13
  import { Track } from '../track.js';
14
- import { BLOCKER_KINDS, BLOCKER_SCOPES, DECISION_KINDS, DISPOSITIONS, EVIDENCE_KINDS, GATES, ITEM_KINDS, ITEM_ROLES, REALIZE_TARGETS, RESOLUTION_RULES, RESULTS, ROLE_CHANGE_TARGETS, SPEC_TARGETS, } from '../ingest/contract.js';
14
+ import { BLOCKER_KINDS, BLOCKER_SCOPES, DECISION_KINDS, DISPOSITIONS, EVIDENCE_KINDS, GATES, ITEM_KINDS, ITEM_ROLES, REALIZE_TARGETS, REOPEN_MOTIVE_VALUES, RESOLUTION_RULES, RESULTS, ROLE_CHANGE_TARGETS, SPEC_TARGETS, } from '../ingest/contract.js';
15
15
  import { ingest } from '../ingest/ingest.js';
16
16
  import { applyRestructurePlan } from './restructure-apply.js';
17
17
  import { TrackReader } from '../read/contract.js';
18
- import { queryText, reportHtml, reportInline, reportText, statusText } from '../read/commands.js';
18
+ import { queryText, reportInline, reportText, resolveHandle, statusText } from '../read/commands.js';
19
19
  import { STATUS_LEVELS } from '../report/status-by-level.js';
20
20
  import { renderSnapshot } from '../report/snapshot.js';
21
21
  import { VERSION } from '../version.js';
22
22
  import { durableWorkspaceId } from '../workspace-id.js';
23
23
  import { desyncFindings } from './desync.js';
24
24
  const USAGE = `usage: track <command>
25
+ --help | help
26
+ global read/store override: --track-dir <directory-containing-events.jsonl> (or TRACK_DIR)
27
+ may appear before or after the command; it redirects reads AND writes to that directory
25
28
  --version | -v
26
29
  init
27
- item new --kind <feature|bug|chore> --title <t> --workspace <w> [--body <b>] [--parent <id>] [--role <workpackage|spec-phase|stream>] [--accountable <a>] [--responsible <a,a>] [--engagement-ref <e>]
30
+ item new --kind <feature|bug|chore> --title <t> --workspace <w> [--body <b>] [--parent <id>] [--role <workpackage|spec-phase|stream>] [--accountable <a>] [--responsible <a,a>] (trim actor IDs; blank responsible members are dropped)
28
31
  item reparent <itemId> [--parent <pid>] [--detach]
32
+ item set-raci <itemId> [--accountable <a>] [--responsible <a,a>] [--client-token <t>] (trimmed blank members are rejected, unlike item new)
29
33
  item set-role <itemId> <workpackage|stream>
30
34
  item scope-declare <itemId> [--allowed <glob,glob>] [--forbidden <...>] [--conditional <...>] [--scope <json>]
31
35
  item spec-amend <itemId> --base-hash <h> --result-hash <h> --patch <json> [--decision-id <id>] [--live-doc-ref <r>] [--proposal-ref <r>] [--summary <s>] [--client-token <t>]
32
36
  item spec <itemId> <to-specify|specified>
33
37
  item realize <itemId> <in-progress|done|cancelled>
38
+ item reopen <itemId> --motive <closed-without-owner-uat|regression-observed> --reason <r> [--client-token <t>]
34
39
  item assign-code <itemId> --code <c> [--client-token <t>]
35
40
  item show <itemId>
36
41
  item ls [--workspace <w>] [--kind <feature|bug|chore>] [--format json|text|md]
@@ -41,7 +46,7 @@ const USAGE = `usage: track <command>
41
46
  decision dossier <decisionId> [--context <c>] [--options-json <json> --recommendation <optionId> --rationale <r>]
42
47
  decision disposition <itemId> <orientation|commitment> <required|skipped|not-applicable>
43
48
  decision add-artifact <decisionId> --kind <h2a-decision-dossier|rendered-view|mockup> [--negotiation-ref <n>] [--dossier-hash <h>] [--view-ref <v>] [--source-dossier-hash <h>] [--label <l>] [--client-token <t>]
44
- blocker raise --target <id> --kind <decision|dependency> [--ref <id>] [--reason <r>] [--rule <linked-done|linked-accepted|manual>] [--scope <intra|extra>] [--engagement-ref <e>]
49
+ blocker raise --target <id> --kind <decision|dependency> [--ref <id>] [--reason <r>] [--rule <linked-done|linked-accepted|manual>] [--scope <intra|extra>] [--engagement-ref <e>] [--owner <actor>]
45
50
  blocker resolve <blockerId>
46
51
  blocker resolve-external --engagement-ref <e>
47
52
  accept criterion <itemId> --statement <s>
@@ -51,7 +56,7 @@ const USAGE = `usage: track <command>
51
56
  accept waive <criterionId> --reason <r>
52
57
  consolidate --items <id,id> --commit <mergeCommit> [--client-token <t>]
53
58
  priority assess <itemId> --ubv <n> --tc <n> --rr <n> --js <n>
54
- report [--decisions] [--require-accepted] [--active-roster] [--wp|--flat] [--inline] [--width <n>] [--level <spec|plan|wp|lot|task>] [--raw] [--format json|text|md|html] [--commit <sha>]
59
+ report [--scope <container-id|code|label>] [--since <sha|YYYY-MM-DD> [--until <sha|YYYY-MM-DD>]|--period <today|week|month|all>] [--decisions] [--require-accepted] [--active-roster] [--wp|--flat] [--inline] [--width <n>] [--level <spec|plan|wp|lot|task>] [--raw] [--resolve <handle>] [--sub-wp] [--format json|text|md] [--commit <sha>] [--now <iso>]
55
60
  snapshot [--require-accepted] [--format json|text|md] [--commit <sha>]
56
61
  export-graph [--repo-key <repo:key>] [--source-id <id>] [--observed-at <iso>]
57
62
  query [--kind <k>] [--role <workpackage|spec-phase|stream>] [--workspace <w>] [--bucket <AWAITED|DROPPED|DONE|TO-DO>] [--realization <r>] [--acceptance <a>] [--format json|text|md] [--commit <sha>]
@@ -127,6 +132,98 @@ function resolveCommit(cwd, c) {
127
132
  return c;
128
133
  }
129
134
  }
135
+ /** A calendar date is a local civil day at the CLI boundary, then emitted as an absolute instant. */
136
+ function resolveReportDate(raw, upper) {
137
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/u.exec(raw);
138
+ if (match === null)
139
+ return undefined;
140
+ const year = Number(match[1]);
141
+ const month = Number(match[2]);
142
+ const day = Number(match[3]);
143
+ const value = new Date(year, month - 1, day, upper ? 23 : 0, upper ? 59 : 0, upper ? 59 : 0, upper ? 999 : 0);
144
+ if (value.getFullYear() !== year || value.getMonth() !== month - 1 || value.getDate() !== day)
145
+ return undefined;
146
+ return value.toISOString();
147
+ }
148
+ /** Resolve a date or commit selector strictly: period bounds must never silently degrade into a literal. */
149
+ function resolveReportBoundary(cwd, raw, flag) {
150
+ const date = resolveReportDate(raw, flag === '--until');
151
+ if (date !== undefined)
152
+ return { at: date, ref: null };
153
+ try {
154
+ const ref = execFileSync('git', ['rev-parse', '--verify', '--end-of-options', `${raw}^{commit}`], {
155
+ cwd,
156
+ encoding: 'utf8',
157
+ stdio: ['ignore', 'pipe', 'ignore'],
158
+ }).trim();
159
+ const at = execFileSync('git', ['show', '-s', '--format=%cI', ref], {
160
+ cwd,
161
+ encoding: 'utf8',
162
+ stdio: ['ignore', 'pipe', 'ignore'],
163
+ }).trim();
164
+ if (Number.isNaN(Date.parse(at)))
165
+ throw new Error('invalid committer date');
166
+ return { at, ref };
167
+ }
168
+ catch {
169
+ throw new DomainError(`${flag} must be a YYYY-MM-DD date or a resolvable commit`);
170
+ }
171
+ }
172
+ function localDayStart(value) {
173
+ return new Date(value.getFullYear(), value.getMonth(), value.getDate());
174
+ }
175
+ function localDayEnd(value) {
176
+ return new Date(value.getFullYear(), value.getMonth(), value.getDate(), 23, 59, 59, 999);
177
+ }
178
+ function namedReportPeriod(raw, now) {
179
+ const clock = new Date(now);
180
+ if (Number.isNaN(clock.getTime()))
181
+ throw new DomainError('--now must be an ISO timestamp');
182
+ if (raw === 'all')
183
+ return { requested: raw, fromRef: null, toRef: null };
184
+ let from;
185
+ let to;
186
+ if (raw === 'today') {
187
+ from = localDayStart(clock);
188
+ to = localDayEnd(clock);
189
+ }
190
+ else if (raw === 'week') {
191
+ const start = localDayStart(clock);
192
+ start.setDate(start.getDate() - ((start.getDay() + 6) % 7)); // Monday, in local time
193
+ from = start;
194
+ to = new Date(start.getFullYear(), start.getMonth(), start.getDate() + 6, 23, 59, 59, 999);
195
+ }
196
+ else {
197
+ from = new Date(clock.getFullYear(), clock.getMonth(), 1);
198
+ to = new Date(clock.getFullYear(), clock.getMonth() + 1, 0, 23, 59, 59, 999);
199
+ }
200
+ return { requested: raw, from: from.toISOString(), to: to.toISOString(), fromRef: null, toRef: null };
201
+ }
202
+ function resolveReportPeriod(flags, cwd, now) {
203
+ const since = opt(flags, 'since');
204
+ const until = opt(flags, 'until');
205
+ const named = opt(flags, 'period');
206
+ if (since !== undefined && named !== undefined)
207
+ throw new DomainError('--since and --period are mutually exclusive');
208
+ if (until !== undefined && since === undefined)
209
+ throw new DomainError('--until requires --since');
210
+ if (named !== undefined)
211
+ return namedReportPeriod(oneOf(named, ['today', 'week', 'month', 'all'], '--period'), now);
212
+ if (since === undefined)
213
+ return undefined;
214
+ const lower = resolveReportBoundary(cwd, since, '--since');
215
+ const upper = until === undefined ? undefined : resolveReportBoundary(cwd, until, '--until');
216
+ if (upper !== undefined && Date.parse(lower.at) > Date.parse(upper.at)) {
217
+ throw new DomainError('--since must not be after --until');
218
+ }
219
+ return {
220
+ requested: until === undefined ? since : `${since}..${until}`,
221
+ from: lower.at,
222
+ ...(upper !== undefined ? { to: upper.at } : {}),
223
+ fromRef: lower.ref,
224
+ toRef: upper?.ref ?? null,
225
+ };
226
+ }
130
227
  function remoteKeyFromUrl(remoteUrl) {
131
228
  const trimmed = remoteUrl.trim().replace(/\.git$/i, '');
132
229
  const sshUrl = trimmed.match(/^ssh:\/\/(?:[^@]+@)?([^/\s]+)(\/.+)$/i);
@@ -305,10 +402,32 @@ function extractTrackDirFlag(argv) {
305
402
  }
306
403
  return trackDirFlag !== undefined ? { trackDirFlag, rest } : { rest };
307
404
  }
405
+ const REPORT_USAGE = `usage: track report [--scope <container-id|code|label>] [--since <sha|YYYY-MM-DD> [--until <sha|YYYY-MM-DD>] | --period <today|week|month|all>] [--raw] [--wp] [--flat] [--inline|--width <40..240>] [--decisions] [--active-roster] [--require-accepted] [--resolve <handle>] [--commit <sha>] [--now <iso>] [--sub-wp] [--format json|text|md] [--track-dir <directory-containing-events.jsonl>]
406
+
407
+ --scope selects one exact role-container by its id, durable assigned code, or current derived label. The selected container and all descendants are rendered through the same four-section report; unknown or ambiguous selectors fail loudly.
408
+
409
+ --resolve <handle> resolves a report handle (a positional [n.m] row handle, or a D#/Q# dossier number) back to its item id. It is the one command the report documents for acting on a row without printing a ULID in a column. Handles are positional and per-report: resolve them against the same log and baseline the report was rendered from.
410
+
411
+ --sub-wp lists sub-WP rows beside their parent. Without it, sub-levels are aggregated into their parent on a long window (>= 14 days) and listed on a short one — the WP is the reading unit of a long report.
412
+
413
+ --now <iso> pins the clock used by a named period and, without a selector, the whole-log upper bound. Without it, whole-log and --since reports stop at the journal head.
414
+
415
+ --since accepts a local YYYY-MM-DD date or a git commit (using its committer date); --until closes a --since range. --period today|week|month|all is the named alternative. --since and --period are mutually exclusive. A selected period changes FAIT only: À-FAIRE remains the full current open-work projection.
416
+
417
+ --track-dir is a global override and may appear before or after the command. It selects the directory that contains events.jsonl; it is especially useful for a read-only fixture. TRACK_DIR is the environment equivalent.
418
+ `;
308
419
  export function runCli(rawArgv, io) {
309
420
  const { trackDirFlag, rest: argv } = extractTrackDirFlag(rawArgv);
310
421
  const cmd = argv[0];
311
422
  const rest = argv.slice(1);
423
+ if (cmd === '--help' || cmd === 'help' || cmd === undefined) {
424
+ io.out(USAGE);
425
+ return 0;
426
+ }
427
+ if (cmd === 'report' && rest.length === 1 && rest[0] === '--help') {
428
+ io.out(REPORT_USAGE);
429
+ return 0;
430
+ }
312
431
  const trackDirEnv = process.env['TRACK_DIR'];
313
432
  const resolveOpts = {
314
433
  cwd: io.cwd,
@@ -507,6 +626,26 @@ function cmdItem(args, ctx) {
507
626
  io.out('ok\n');
508
627
  return 0;
509
628
  }
629
+ if (sub === 'set-raci') {
630
+ const accountable = opt(flags, 'accountable');
631
+ const responsible = opt(flags, 'responsible');
632
+ if (accountable === undefined && responsible === undefined) {
633
+ throw new DomainError('item set-raci requires --accountable and/or --responsible');
634
+ }
635
+ const clientToken = opt(flags, 'client-token');
636
+ if (clientToken !== undefined && store(ctx).readAll().some((event) => event.clientToken === clientToken)) {
637
+ io.out('no-op: client-token already applied\n');
638
+ return 0;
639
+ }
640
+ track.setRaci(positional[0], {
641
+ ...(accountable !== undefined ? { accountable } : {}),
642
+ ...(responsible !== undefined
643
+ ? { responsible: responsible.split(',').map((s) => s.trim()) }
644
+ : {}),
645
+ }, clientToken);
646
+ io.out('ok\n');
647
+ return 0;
648
+ }
510
649
  if (sub === 'scope-declare') {
511
650
  // Scope §B(a) — declare INERT path-scope globs on a WP/spec-phase. A comma-separated glob list per
512
651
  // axis (`--allowed`/`--forbidden`/`--conditional`), OR a `--scope <json>` object; the two are
@@ -598,6 +737,23 @@ function cmdItem(args, ctx) {
598
737
  io.out('ok\n');
599
738
  return 0;
600
739
  }
740
+ if (sub === 'reopen') {
741
+ // Regression expression — reopen a terminally-closed item (done/cancelled → in-progress) WITH its motive.
742
+ // Deliberately a SEPARATE verb from `item realize`, which keeps done/cancelled terminal. `--client-token`
743
+ // gives append-once idempotency at the CLI boundary (mirrors `assign-code`/`spec-amend`): without it a
744
+ // retry would hit "not closed" — the item is already reopened — instead of reading as the no-op it is.
745
+ const itemId = positional[0];
746
+ const motive = oneOf(req(flags, 'motive'), REOPEN_MOTIVE_VALUES, '--motive');
747
+ const reason = req(flags, 'reason');
748
+ const clientToken = opt(flags, 'client-token');
749
+ if (clientToken !== undefined && store(ctx).readAll().some((e) => e.clientToken === clientToken)) {
750
+ io.out('no-op: client-token already applied\n');
751
+ return 0;
752
+ }
753
+ track.reopenItem(itemId, { motive, reason }, clientToken);
754
+ io.out(`reopened ${itemId} (${motive})\n`);
755
+ return 0;
756
+ }
601
757
  if (sub === 'assign-code') {
602
758
  // A1 (wp-codes) — assign a durable, re-assignable display `code` to a workpackage/spec-phase (the
603
759
  // canonical write; `assignCode` enforces roster-global uniqueness, re-asserted under the lock).
@@ -625,7 +781,7 @@ function cmdItem(args, ctx) {
625
781
  rowsOut(rows, fmt(flags), io);
626
782
  return 0;
627
783
  }
628
- io.err('usage: track item <new|reparent|set-role|scope-declare|spec-amend|spec|realize|assign-code|show|ls>\n');
784
+ io.err('usage: track item <new|reparent|set-raci|set-role|scope-declare|spec-amend|spec|realize|assign-code|show|ls>\n');
629
785
  return 2;
630
786
  }
631
787
  /**
@@ -782,6 +938,11 @@ function cmdBlocker(args, ctx) {
782
938
  const { positional, flags } = parseFlags(args.slice(1));
783
939
  const track = writeTrack(ctx);
784
940
  if (sub === 'raise') {
941
+ const rawOwner = opt(flags, 'owner');
942
+ const owner = rawOwner?.trim();
943
+ if (rawOwner !== undefined && owner === '') {
944
+ throw new DomainError('blocker raise: --owner must be a non-empty actor');
945
+ }
785
946
  const id = track.openBlocker({
786
947
  targetId: req(flags, 'target'),
787
948
  kind: oneOf(req(flags, 'kind'), BLOCKER_KINDS, '--kind'),
@@ -794,6 +955,7 @@ function cmdBlocker(args, ctx) {
794
955
  ? { scope: oneOf(req(flags, 'scope'), BLOCKER_SCOPES, '--scope') }
795
956
  : {}),
796
957
  ...(opt(flags, 'engagement-ref') !== undefined ? { engagementRef: req(flags, 'engagement-ref') } : {}),
958
+ ...(owner !== undefined ? { owner } : {}),
797
959
  });
798
960
  io.out(`${id}\n`);
799
961
  return 0;
@@ -911,8 +1073,18 @@ function cmdReport(args, ctx) {
911
1073
  const { positional, flags } = parseFlags(args);
912
1074
  if (positional.length > 0)
913
1075
  throw new DomainError(`unexpected report argument(s): ${positional.join(' ')}`);
914
- for (const name of ['commit', 'format', 'level', 'width'])
1076
+ for (const name of ['commit', 'format', 'level', 'width', 'resolve', 'now', 'scope', 'since', 'until', 'period'])
915
1077
  assertValueFlag(flags, name);
1078
+ const scope = opt(flags, 'scope');
1079
+ // Criterion 10b — the one documented command that turns a short report handle back into an item.
1080
+ if (opt(flags, 'resolve') !== undefined) {
1081
+ assertOnlyFlags(flags, ['resolve', 'scope', 'commit', 'require-accepted']);
1082
+ io.out(resolveHandle(new TrackReader(ctx.eventsPath), {
1083
+ baselineCommit: resolveCommit(io.cwd, opt(flags, 'commit')),
1084
+ requireAccepted: assertBooleanFlag(flags, 'require-accepted'),
1085
+ }, req(flags, 'resolve'), scope));
1086
+ return 0;
1087
+ }
916
1088
  const raw = assertBooleanFlag(flags, 'raw');
917
1089
  if (raw) {
918
1090
  assertOnlyFlags(flags, ['raw', 'commit', 'require-accepted', 'format']);
@@ -930,16 +1102,17 @@ function cmdReport(args, ctx) {
930
1102
  return 0;
931
1103
  }
932
1104
  assertOnlyFlags(flags, [
933
- 'commit', 'require-accepted', 'decisions', 'active-roster', 'wp', 'flat', 'inline', 'width', 'format',
1105
+ 'commit', 'require-accepted', 'decisions', 'active-roster', 'wp', 'flat', 'inline', 'width', 'format', 'now',
1106
+ 'sub-wp', 'scope', 'since', 'until', 'period',
934
1107
  ]);
935
1108
  const rawFormat = opt(flags, 'format');
936
- if (rawFormat !== undefined && !['json', 'text', 'md', 'html'].includes(rawFormat)) {
937
- throw new DomainError('--format must be one of: json|text|md|html');
1109
+ if (rawFormat !== undefined && !['json', 'text', 'md'].includes(rawFormat)) {
1110
+ throw new DomainError('--format must be one of: json|text|md');
938
1111
  }
939
1112
  const widthArg = opt(flags, 'width');
940
1113
  const inlineFlag = assertBooleanFlag(flags, 'inline');
941
1114
  const inline = inlineFlag || widthArg !== undefined;
942
- const format = oneOf(rawFormat ?? 'text', ['json', 'text', 'md', 'html'], '--format');
1115
+ const format = oneOf(rawFormat ?? 'text', ['json', 'text', 'md'], '--format');
943
1116
  if (inline && format !== 'text')
944
1117
  throw new DomainError('--inline/--width accepts no --format, or --format text');
945
1118
  const requireAccepted = assertBooleanFlag(flags, 'require-accepted');
@@ -951,8 +1124,10 @@ function cmdReport(args, ctx) {
951
1124
  throw new DomainError('--wp and --flat are mutually exclusive');
952
1125
  if (format === 'json' && flat)
953
1126
  throw new DomainError('--flat is only meaningful for text or md reports');
954
- if (format === 'html' && flat)
955
- throw new DomainError('--format html is always the deterministic conductor and rejects --flat');
1127
+ if (scope !== undefined && flat)
1128
+ throw new DomainError('--scope requires the four-section conductor and rejects --flat');
1129
+ if (scope !== undefined && activeRoster)
1130
+ throw new DomainError('--scope includes the complete subtree and rejects --active-roster');
956
1131
  let width;
957
1132
  if (widthArg !== undefined) {
958
1133
  if (!/^\d+$/u.test(widthArg))
@@ -961,6 +1136,11 @@ function cmdReport(args, ctx) {
961
1136
  if (width < 40 || width > 240)
962
1137
  throw new DomainError('--width must be an integer in [40,240]');
963
1138
  }
1139
+ if (scope !== undefined && inline)
1140
+ throw new DomainError('--scope preserves the validated report shape and rejects --inline/--width');
1141
+ if (scope !== undefined && format === 'md') {
1142
+ throw new DomainError('--scope currently supports the owner text report and JSON projection only');
1143
+ }
964
1144
  // Every format is a deterministic read over the folded log. The default human view is
965
1145
  // the conductor; --flat explicitly requests the legacy bucket projection. JSON preserves
966
1146
  // its established flat machine contract unless --wp is explicit, so --flat is rejected there
@@ -971,19 +1151,28 @@ function cmdReport(args, ctx) {
971
1151
  requireAccepted,
972
1152
  // The owner-facing conductor always classifies decision dossiers. JSON keeps its
973
1153
  // established opt-in decision payload, while --flat retains the legacy opt-in.
974
- decisions: requestedDecisions || (!flat && format !== 'json'),
975
- wpTree: wp || (!flat && format !== 'json'),
1154
+ decisions: scope !== undefined || requestedDecisions || (!flat && format !== 'json'),
1155
+ wpTree: scope !== undefined || wp || (!flat && format !== 'json'),
976
1156
  activeRoster,
977
1157
  };
1158
+ // Criterion 21 — with no selector the report runs from the first event to the journal head; an injected
1159
+ // clock is an explicit reproducible upper bound. Named calendar periods use the wall clock only to turn
1160
+ // `today`/`week`/`month` into absolute local-time bounds, while the library itself remains clockless.
1161
+ const nowArg = opt(flags, 'now');
1162
+ if (nowArg !== undefined && Number.isNaN(new Date(nowArg).getTime())) {
1163
+ throw new DomainError('--now must be an ISO timestamp');
1164
+ }
1165
+ const periodClock = nowArg ?? new Date().toISOString();
1166
+ const period = resolveReportPeriod(flags, io.cwd, periodClock);
1167
+ // Criterion 25 — the EXPLICIT owner request for sub-WP rows. Without it the reading unit follows the
1168
+ // window: the WP on a long one, the sub-level on a short one.
1169
+ const subWp = assertBooleanFlag(flags, 'sub-wp');
978
1170
  const reader = new TrackReader(ctx.eventsPath);
979
1171
  if (inline) {
980
- io.out(reportInline(reader, options, width === undefined ? {} : { width }));
981
- }
982
- else if (format === 'html') {
983
- io.out(reportHtml(reader, options));
1172
+ io.out(reportInline(reader, options, width === undefined ? {} : { width }, nowArg, period));
984
1173
  }
985
1174
  else {
986
- io.out(reportText(reader, options, format));
1175
+ io.out(reportText(reader, options, format, nowArg, subWp, scope, period));
987
1176
  }
988
1177
  return 0;
989
1178
  }