@sentropic/track 0.88.0 → 0.90.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 (72) hide show
  1. package/dist/cli/index.d.ts.map +1 -1
  2. package/dist/cli/index.js +175 -28
  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 +23 -11
  34. package/dist/read/commands.d.ts.map +1 -1
  35. package/dist/read/commands.js +142 -36
  36. package/dist/read/commands.js.map +1 -1
  37. package/dist/read/contract.d.ts +21 -3
  38. package/dist/read/contract.d.ts.map +1 -1
  39. package/dist/read/contract.js +26 -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/format.d.ts +55 -1
  45. package/dist/report/format.d.ts.map +1 -1
  46. package/dist/report/format.js +203 -72
  47. package/dist/report/format.js.map +1 -1
  48. package/dist/report/index.d.ts +0 -1
  49. package/dist/report/index.d.ts.map +1 -1
  50. package/dist/report/index.js +0 -1
  51. package/dist/report/index.js.map +1 -1
  52. package/dist/state/fold.d.ts.map +1 -1
  53. package/dist/state/fold.js +48 -0
  54. package/dist/state/fold.js.map +1 -1
  55. package/dist/track.d.ts +34 -1
  56. package/dist/track.d.ts.map +1 -1
  57. package/dist/track.js +95 -2
  58. package/dist/track.js.map +1 -1
  59. package/package.json +2 -2
  60. package/skills/track-operation/SKILL.md +16 -4
  61. package/dist/report/ai-report.d.ts +0 -122
  62. package/dist/report/ai-report.d.ts.map +0 -1
  63. package/dist/report/ai-report.js +0 -727
  64. package/dist/report/ai-report.js.map +0 -1
  65. package/dist/report/html.d.ts +0 -9
  66. package/dist/report/html.d.ts.map +0 -1
  67. package/dist/report/html.js +0 -73
  68. package/dist/report/html.js.map +0 -1
  69. package/dist/report/present.d.ts +0 -28
  70. package/dist/report/present.d.ts.map +0 -1
  71. package/dist/report/present.js +0 -32
  72. 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;AAqTD,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA8J7E"}
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,11 +11,11 @@ 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, resolveHandle, 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';
@@ -27,13 +27,15 @@ const USAGE = `usage: track <command>
27
27
  may appear before or after the command; it redirects reads AND writes to that directory
28
28
  --version | -v
29
29
  init
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>] [--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)
31
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)
32
33
  item set-role <itemId> <workpackage|stream>
33
34
  item scope-declare <itemId> [--allowed <glob,glob>] [--forbidden <...>] [--conditional <...>] [--scope <json>]
34
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>]
35
36
  item spec <itemId> <to-specify|specified>
36
37
  item realize <itemId> <in-progress|done|cancelled>
38
+ item reopen <itemId> --motive <closed-without-owner-uat|regression-observed> --reason <r> [--client-token <t>]
37
39
  item assign-code <itemId> --code <c> [--client-token <t>]
38
40
  item show <itemId>
39
41
  item ls [--workspace <w>] [--kind <feature|bug|chore>] [--format json|text|md]
@@ -44,7 +46,7 @@ const USAGE = `usage: track <command>
44
46
  decision dossier <decisionId> [--context <c>] [--options-json <json> --recommendation <optionId> --rationale <r>]
45
47
  decision disposition <itemId> <orientation|commitment> <required|skipped|not-applicable>
46
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>]
47
- 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>]
48
50
  blocker resolve <blockerId>
49
51
  blocker resolve-external --engagement-ref <e>
50
52
  accept criterion <itemId> --statement <s>
@@ -54,7 +56,7 @@ const USAGE = `usage: track <command>
54
56
  accept waive <criterionId> --reason <r>
55
57
  consolidate --items <id,id> --commit <mergeCommit> [--client-token <t>]
56
58
  priority assess <itemId> --ubv <n> --tc <n> --rr <n> --js <n>
57
- report [--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|html] [--commit <sha>] [--now <iso>]
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>]
58
60
  snapshot [--require-accepted] [--format json|text|md] [--commit <sha>]
59
61
  export-graph [--repo-key <repo:key>] [--source-id <id>] [--observed-at <iso>]
60
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>]
@@ -130,6 +132,98 @@ function resolveCommit(cwd, c) {
130
132
  return c;
131
133
  }
132
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
+ }
133
227
  function remoteKeyFromUrl(remoteUrl) {
134
228
  const trimmed = remoteUrl.trim().replace(/\.git$/i, '');
135
229
  const sshUrl = trimmed.match(/^ssh:\/\/(?:[^@]+@)?([^/\s]+)(\/.+)$/i);
@@ -308,13 +402,17 @@ function extractTrackDirFlag(argv) {
308
402
  }
309
403
  return trackDirFlag !== undefined ? { trackDirFlag, rest } : { rest };
310
404
  }
311
- const REPORT_USAGE = `usage: track report [--raw] [--wp] [--flat] [--inline|--width <40..240>] [--decisions] [--active-roster] [--require-accepted] [--resolve <handle>] [--commit <sha>] [--now <iso>] [--sub-wp] [--format json|text|md|html] [--track-dir <directory-containing-events.jsonl>]
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.
312
408
 
313
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.
314
410
 
315
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.
316
412
 
317
- --now <iso> pins the window's upper bound (default: the wall clock). The report's period always runs from the first recorded event to that bound; pin it to reproduce a committed fixture byte for byte.
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.
318
416
 
319
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.
320
418
  `;
@@ -528,6 +626,26 @@ function cmdItem(args, ctx) {
528
626
  io.out('ok\n');
529
627
  return 0;
530
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
+ }
531
649
  if (sub === 'scope-declare') {
532
650
  // Scope §B(a) — declare INERT path-scope globs on a WP/spec-phase. A comma-separated glob list per
533
651
  // axis (`--allowed`/`--forbidden`/`--conditional`), OR a `--scope <json>` object; the two are
@@ -619,6 +737,23 @@ function cmdItem(args, ctx) {
619
737
  io.out('ok\n');
620
738
  return 0;
621
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
+ }
622
757
  if (sub === 'assign-code') {
623
758
  // A1 (wp-codes) — assign a durable, re-assignable display `code` to a workpackage/spec-phase (the
624
759
  // canonical write; `assignCode` enforces roster-global uniqueness, re-asserted under the lock).
@@ -646,7 +781,7 @@ function cmdItem(args, ctx) {
646
781
  rowsOut(rows, fmt(flags), io);
647
782
  return 0;
648
783
  }
649
- 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');
650
785
  return 2;
651
786
  }
652
787
  /**
@@ -803,6 +938,11 @@ function cmdBlocker(args, ctx) {
803
938
  const { positional, flags } = parseFlags(args.slice(1));
804
939
  const track = writeTrack(ctx);
805
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
+ }
806
946
  const id = track.openBlocker({
807
947
  targetId: req(flags, 'target'),
808
948
  kind: oneOf(req(flags, 'kind'), BLOCKER_KINDS, '--kind'),
@@ -815,6 +955,7 @@ function cmdBlocker(args, ctx) {
815
955
  ? { scope: oneOf(req(flags, 'scope'), BLOCKER_SCOPES, '--scope') }
816
956
  : {}),
817
957
  ...(opt(flags, 'engagement-ref') !== undefined ? { engagementRef: req(flags, 'engagement-ref') } : {}),
958
+ ...(owner !== undefined ? { owner } : {}),
818
959
  });
819
960
  io.out(`${id}\n`);
820
961
  return 0;
@@ -932,15 +1073,16 @@ function cmdReport(args, ctx) {
932
1073
  const { positional, flags } = parseFlags(args);
933
1074
  if (positional.length > 0)
934
1075
  throw new DomainError(`unexpected report argument(s): ${positional.join(' ')}`);
935
- for (const name of ['commit', 'format', 'level', 'width', 'resolve', 'now'])
1076
+ for (const name of ['commit', 'format', 'level', 'width', 'resolve', 'now', 'scope', 'since', 'until', 'period'])
936
1077
  assertValueFlag(flags, name);
1078
+ const scope = opt(flags, 'scope');
937
1079
  // Criterion 10b — the one documented command that turns a short report handle back into an item.
938
1080
  if (opt(flags, 'resolve') !== undefined) {
939
- assertOnlyFlags(flags, ['resolve', 'commit', 'require-accepted']);
1081
+ assertOnlyFlags(flags, ['resolve', 'scope', 'commit', 'require-accepted']);
940
1082
  io.out(resolveHandle(new TrackReader(ctx.eventsPath), {
941
1083
  baselineCommit: resolveCommit(io.cwd, opt(flags, 'commit')),
942
1084
  requireAccepted: assertBooleanFlag(flags, 'require-accepted'),
943
- }, req(flags, 'resolve')));
1085
+ }, req(flags, 'resolve'), scope));
944
1086
  return 0;
945
1087
  }
946
1088
  const raw = assertBooleanFlag(flags, 'raw');
@@ -961,16 +1103,16 @@ function cmdReport(args, ctx) {
961
1103
  }
962
1104
  assertOnlyFlags(flags, [
963
1105
  'commit', 'require-accepted', 'decisions', 'active-roster', 'wp', 'flat', 'inline', 'width', 'format', 'now',
964
- 'sub-wp',
1106
+ 'sub-wp', 'scope', 'since', 'until', 'period',
965
1107
  ]);
966
1108
  const rawFormat = opt(flags, 'format');
967
- if (rawFormat !== undefined && !['json', 'text', 'md', 'html'].includes(rawFormat)) {
968
- 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');
969
1111
  }
970
1112
  const widthArg = opt(flags, 'width');
971
1113
  const inlineFlag = assertBooleanFlag(flags, 'inline');
972
1114
  const inline = inlineFlag || widthArg !== undefined;
973
- const format = oneOf(rawFormat ?? 'text', ['json', 'text', 'md', 'html'], '--format');
1115
+ const format = oneOf(rawFormat ?? 'text', ['json', 'text', 'md'], '--format');
974
1116
  if (inline && format !== 'text')
975
1117
  throw new DomainError('--inline/--width accepts no --format, or --format text');
976
1118
  const requireAccepted = assertBooleanFlag(flags, 'require-accepted');
@@ -982,8 +1124,10 @@ function cmdReport(args, ctx) {
982
1124
  throw new DomainError('--wp and --flat are mutually exclusive');
983
1125
  if (format === 'json' && flat)
984
1126
  throw new DomainError('--flat is only meaningful for text or md reports');
985
- if (format === 'html' && flat)
986
- 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');
987
1131
  let width;
988
1132
  if (widthArg !== undefined) {
989
1133
  if (!/^\d+$/u.test(widthArg))
@@ -992,6 +1136,11 @@ function cmdReport(args, ctx) {
992
1136
  if (width < 40 || width > 240)
993
1137
  throw new DomainError('--width must be an integer in [40,240]');
994
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
+ }
995
1144
  // Every format is a deterministic read over the folded log. The default human view is
996
1145
  // the conductor; --flat explicitly requests the legacy bucket projection. JSON preserves
997
1146
  // its established flat machine contract unless --wp is explicit, so --flat is rejected there
@@ -1002,30 +1151,28 @@ function cmdReport(args, ctx) {
1002
1151
  requireAccepted,
1003
1152
  // The owner-facing conductor always classifies decision dossiers. JSON keeps its
1004
1153
  // established opt-in decision payload, while --flat retains the legacy opt-in.
1005
- decisions: requestedDecisions || (!flat && format !== 'json'),
1006
- wpTree: wp || (!flat && format !== 'json'),
1154
+ decisions: scope !== undefined || requestedDecisions || (!flat && format !== 'json'),
1155
+ wpTree: scope !== undefined || wp || (!flat && format !== 'json'),
1007
1156
  activeRoster,
1008
1157
  };
1009
- // Criterion 21 — the report's window runs from the first recorded event to NOW. The clock is injected
1010
- // HERE (the same boundary pattern as `workspace-activity --now`) so the library stays clockless and a
1011
- // committed fixture stays byte-reproducible by pinning `--now`.
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.
1012
1161
  const nowArg = opt(flags, 'now');
1013
1162
  if (nowArg !== undefined && Number.isNaN(new Date(nowArg).getTime())) {
1014
1163
  throw new DomainError('--now must be an ISO timestamp');
1015
1164
  }
1016
- const now = nowArg ?? new Date().toISOString();
1165
+ const periodClock = nowArg ?? new Date().toISOString();
1166
+ const period = resolveReportPeriod(flags, io.cwd, periodClock);
1017
1167
  // Criterion 25 — the EXPLICIT owner request for sub-WP rows. Without it the reading unit follows the
1018
1168
  // window: the WP on a long one, the sub-level on a short one.
1019
1169
  const subWp = assertBooleanFlag(flags, 'sub-wp');
1020
1170
  const reader = new TrackReader(ctx.eventsPath);
1021
1171
  if (inline) {
1022
- io.out(reportInline(reader, options, width === undefined ? {} : { width }));
1023
- }
1024
- else if (format === 'html') {
1025
- io.out(reportHtml(reader, options, now, subWp));
1172
+ io.out(reportInline(reader, options, width === undefined ? {} : { width }, nowArg, period));
1026
1173
  }
1027
1174
  else {
1028
- io.out(reportText(reader, options, format, now, subWp));
1175
+ io.out(reportText(reader, options, format, nowArg, subWp, scope, period));
1029
1176
  }
1030
1177
  return 0;
1031
1178
  }