@sentropic/track 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -0
  3. package/dist/accept/index.d.ts +3 -0
  4. package/dist/accept/index.d.ts.map +1 -0
  5. package/dist/accept/index.js +5 -0
  6. package/dist/accept/index.js.map +1 -0
  7. package/dist/accept/ingest.d.ts +16 -0
  8. package/dist/accept/ingest.d.ts.map +1 -0
  9. package/dist/accept/ingest.js +60 -0
  10. package/dist/accept/ingest.js.map +1 -0
  11. package/dist/accept/status.d.ts +15 -0
  12. package/dist/accept/status.d.ts.map +1 -0
  13. package/dist/accept/status.js +44 -0
  14. package/dist/accept/status.js.map +1 -0
  15. package/dist/branch/index.d.ts +2 -0
  16. package/dist/branch/index.d.ts.map +1 -0
  17. package/dist/branch/index.js +3 -0
  18. package/dist/branch/index.js.map +1 -0
  19. package/dist/branch/parse.d.ts +31 -0
  20. package/dist/branch/parse.d.ts.map +1 -0
  21. package/dist/branch/parse.js +116 -0
  22. package/dist/branch/parse.js.map +1 -0
  23. package/dist/cli/desync.d.ts +14 -0
  24. package/dist/cli/desync.d.ts.map +1 -0
  25. package/dist/cli/desync.js +35 -0
  26. package/dist/cli/desync.js.map +1 -0
  27. package/dist/cli/index.d.ts +8 -0
  28. package/dist/cli/index.d.ts.map +1 -0
  29. package/dist/cli/index.js +402 -0
  30. package/dist/cli/index.js.map +1 -0
  31. package/dist/events/canonical.d.ts +5 -0
  32. package/dist/events/canonical.d.ts.map +1 -0
  33. package/dist/events/canonical.js +134 -0
  34. package/dist/events/canonical.js.map +1 -0
  35. package/dist/events/frame.d.ts +11 -0
  36. package/dist/events/frame.d.ts.map +1 -0
  37. package/dist/events/frame.js +19 -0
  38. package/dist/events/frame.js.map +1 -0
  39. package/dist/events/head.d.ts +21 -0
  40. package/dist/events/head.d.ts.map +1 -0
  41. package/dist/events/head.js +54 -0
  42. package/dist/events/head.js.map +1 -0
  43. package/dist/events/index.d.ts +7 -0
  44. package/dist/events/index.d.ts.map +1 -0
  45. package/dist/events/index.js +9 -0
  46. package/dist/events/index.js.map +1 -0
  47. package/dist/events/store.d.ts +33 -0
  48. package/dist/events/store.d.ts.map +1 -0
  49. package/dist/events/store.js +121 -0
  50. package/dist/events/store.js.map +1 -0
  51. package/dist/events/types.d.ts +42 -0
  52. package/dist/events/types.d.ts.map +1 -0
  53. package/dist/events/types.js +22 -0
  54. package/dist/events/types.js.map +1 -0
  55. package/dist/events/validate.d.ts +72 -0
  56. package/dist/events/validate.d.ts.map +1 -0
  57. package/dist/events/validate.js +210 -0
  58. package/dist/events/validate.js.map +1 -0
  59. package/dist/index.d.ts +10 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +11 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/model/acceptance.d.ts +60 -0
  64. package/dist/model/acceptance.d.ts.map +1 -0
  65. package/dist/model/acceptance.js +2 -0
  66. package/dist/model/acceptance.js.map +1 -0
  67. package/dist/model/blocker.d.ts +33 -0
  68. package/dist/model/blocker.d.ts.map +1 -0
  69. package/dist/model/blocker.js +15 -0
  70. package/dist/model/blocker.js.map +1 -0
  71. package/dist/model/decision.d.ts +59 -0
  72. package/dist/model/decision.d.ts.map +1 -0
  73. package/dist/model/decision.js +17 -0
  74. package/dist/model/decision.js.map +1 -0
  75. package/dist/model/index.d.ts +6 -0
  76. package/dist/model/index.d.ts.map +1 -0
  77. package/dist/model/index.js +7 -0
  78. package/dist/model/index.js.map +1 -0
  79. package/dist/model/item.d.ts +51 -0
  80. package/dist/model/item.d.ts.map +1 -0
  81. package/dist/model/item.js +42 -0
  82. package/dist/model/item.js.map +1 -0
  83. package/dist/model/priority.d.ts +22 -0
  84. package/dist/model/priority.d.ts.map +1 -0
  85. package/dist/model/priority.js +13 -0
  86. package/dist/model/priority.js.map +1 -0
  87. package/dist/priority/index.d.ts +2 -0
  88. package/dist/priority/index.d.ts.map +1 -0
  89. package/dist/priority/index.js +4 -0
  90. package/dist/priority/index.js.map +1 -0
  91. package/dist/report/buckets.d.ts +18 -0
  92. package/dist/report/buckets.d.ts.map +1 -0
  93. package/dist/report/buckets.js +24 -0
  94. package/dist/report/buckets.js.map +1 -0
  95. package/dist/report/build.d.ts +44 -0
  96. package/dist/report/build.d.ts.map +1 -0
  97. package/dist/report/build.js +62 -0
  98. package/dist/report/build.js.map +1 -0
  99. package/dist/report/format.d.ts +5 -0
  100. package/dist/report/format.d.ts.map +1 -0
  101. package/dist/report/format.js +81 -0
  102. package/dist/report/format.js.map +1 -0
  103. package/dist/report/index.d.ts +4 -0
  104. package/dist/report/index.d.ts.map +1 -0
  105. package/dist/report/index.js +5 -0
  106. package/dist/report/index.js.map +1 -0
  107. package/dist/state/fold.d.ts +25 -0
  108. package/dist/state/fold.d.ts.map +1 -0
  109. package/dist/state/fold.js +208 -0
  110. package/dist/state/fold.js.map +1 -0
  111. package/dist/state/index.d.ts +3 -0
  112. package/dist/state/index.d.ts.map +1 -0
  113. package/dist/state/index.js +4 -0
  114. package/dist/state/index.js.map +1 -0
  115. package/dist/state/snapshot.d.ts +27 -0
  116. package/dist/state/snapshot.d.ts.map +1 -0
  117. package/dist/state/snapshot.js +49 -0
  118. package/dist/state/snapshot.js.map +1 -0
  119. package/dist/track.d.ts +118 -0
  120. package/dist/track.d.ts.map +1 -0
  121. package/dist/track.js +450 -0
  122. package/dist/track.js.map +1 -0
  123. package/dist/version.d.ts +2 -0
  124. package/dist/version.d.ts.map +1 -0
  125. package/dist/version.js +2 -0
  126. package/dist/version.js.map +1 -0
  127. package/package.json +58 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fabien Antoine
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @sentropic/track
2
+
3
+ Typed product-backlog and **spec / realization / acceptance tracking** for the sentropic ecosystem — a **record-only system of record** with an append-only, integrity-checked event log.
4
+
5
+ > **Status:** MVP implemented (Lots 0–7). `docs-git` backend · single-writer · CLI. h2a coordination, external backends, MCP, multi-writer merge and UI are v2+ (see [`INTENTION.md`](./INTENTION.md), SPEC §9).
6
+
7
+ ## Model (one line)
8
+
9
+ An **Item** carries three orthogonal axes — **specification** (`to-specify → specified`), **realization** (`to-do → in-progress → done | cancelled | rejected`), **acceptance** (computed `pass/fail/unknown/stale/waived`) — plus **blockers** (relations), **prioritization** (versioned, WSJF), and first-class linked **Decisions** (orientation/commitment, with a typed dossier + go/no-go `outcome`). `report` projects done / to-do / awaited / dropped. See [`docs/spec/SPEC.md`](./docs/spec/SPEC.md).
10
+
11
+ ## Install & build
12
+
13
+ ```bash
14
+ npm install
15
+ npm run build # tsc → dist/ ; bin: `track` → dist/cli/index.js
16
+ npm test # vitest
17
+ ```
18
+
19
+ ## CLI
20
+
21
+ State lives in `.track/events.jsonl` (append-only) in the current directory.
22
+
23
+ ```bash
24
+ track init
25
+
26
+ # items
27
+ track item new --kind feature --title "Login" --workspace web [--body <md-path>] [--parent <id>]
28
+ track item spec <itemId> specified
29
+ track item realize <itemId> in-progress|done|cancelled
30
+ track item show <itemId>
31
+ track item ls [--workspace <w>] [--kind <k>] [--format json|text|md]
32
+
33
+ # decisions (orientation / commitment) — open a decision blocker per target until it settles
34
+ track decision new --kind orientation --title "DB?" --workspace web --targets <id,id> [--context <c>]
35
+ track decision outcome <decisionId> go|no-go|deferred # no-go drops non-terminal targets
36
+ track decision dossier <decisionId> --context <c>
37
+ track decision disposition <itemId> orientation|commitment required|skipped|not-applicable
38
+
39
+ # blockers
40
+ track blocker raise --target <id> --kind dependency --ref <id> [--rule linked-done|manual] [--reason <r>]
41
+ track blocker resolve <blockerId> # only a `manual` dependency blocker
42
+
43
+ # acceptance (criteria → evidence → runs / waivers; status is computed vs --commit)
44
+ track accept criterion <itemId> --statement "user can log in"
45
+ track accept link <criterionId> --kind unit|integration|e2e|manual --locator <id>
46
+ track accept run <evidenceId> --result pass|fail [--commit <sha>] [--env <e>] [--runner <r>]
47
+ track accept run --from junit.xml --format junit [--commit <sha>] # ingest a report
48
+ track accept waive <criterionId> --reason "<why>"
49
+
50
+ # prioritization (WSJF = (UBV + TC + RR/OE) / jobSize)
51
+ track priority assess <itemId> --ubv 8 --tc 4 --rr 2 --js 2
52
+
53
+ # views
54
+ track report [--decisions] [--require-accepted] [--format json|text|md] [--commit <sha>]
55
+ track query [--kind <k>] [--workspace <w>] [--bucket AWAITED|DROPPED|DONE|TO-DO] [--realization <r>] [--acceptance <a>] [--format <f>]
56
+
57
+ # integrity + prose↔log desync (§4); never repairs, only reports
58
+ track validate [--commit <sha>]
59
+
60
+ # BRANCH.md → track sidecar (read-only; idempotent; BRANCH.md stays the source of truth)
61
+ track branch import <plan/NN-BRANCH_*.md> [--commit <sha>]
62
+ ```
63
+
64
+ `report` buckets (first match wins): **AWAITED** (any open blocker) › **DROPPED** (cancelled/rejected) › **DONE** (done, and `pass` if `--require-accepted`) › **TO-DO**. `--commit` defaults to the repo git HEAD.
65
+
66
+ ## Integrity
67
+
68
+ The event log is **append-only**; each event carries `contentHash = sha256(canonicalize(core))` and a positional `prevHash` chain + per-aggregate `seq` (faithful to the h2a journal). `track validate` recomputes the chain, the per-aggregate sequence, atomic-batch completeness, and a `head.json` truncation anchor. The contract was frozen after multi-round adversarial review — see [`docs/reviews/lot1-FROZEN.md`](./docs/reviews/lot1-FROZEN.md).
69
+
70
+ ## Library
71
+
72
+ ```ts
73
+ import { Track, EventStore } from '@sentropic/track'
74
+ const track = new Track(new EventStore('.track/events.jsonl'))
75
+ const id = track.createItem({ kind: 'feature', title: 'Login', workspace: 'web' })
76
+ track.setRealization(id, 'in-progress')
77
+ console.log(track.report({ baselineCommit: 'HEAD' }).buckets)
78
+ ```
79
+
80
+ See [`INTENTION.md`](./INTENTION.md) for the full model, boundaries, and rationale; [`docs/plan/PLAN.md`](./docs/plan/PLAN.md) for the build lots; [`docs/reviews/`](./docs/reviews) for the per-lot double reviews.
81
+
82
+ ## License
83
+
84
+ MIT — see [`LICENSE`](./LICENSE).
@@ -0,0 +1,3 @@
1
+ export { acceptanceStatus, criterionStatus, evidenceForCriterion } from './status.js';
2
+ export { parseRunReport, type RunReportEntry, type RunReportFormat } from './ingest.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/accept/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AACrF,OAAO,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,5 @@
1
+ // Acceptance: criterionStatus / acceptanceStatus total cascade, stale vs baselineCommit (SPEC §2.4),
2
+ // and `accept run --from` ingestion (SPEC §6).
3
+ export { acceptanceStatus, criterionStatus, evidenceForCriterion } from './status.js';
4
+ export { parseRunReport } from './ingest.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/accept/index.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,+CAA+C;AAC/C,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AACrF,OAAO,EAAE,cAAc,EAA6C,MAAM,aAAa,CAAA"}
@@ -0,0 +1,16 @@
1
+ import type { RunResult } from '../model/acceptance.js';
2
+ export type RunReportFormat = 'junit' | 'json';
3
+ export interface RunReportEntry {
4
+ locator: string;
5
+ result: RunResult;
6
+ }
7
+ /**
8
+ * Parse a test report into `{locator, result}` entries (SPEC §6 `accept run --from`). Minimal,
9
+ * dependency-free: JUnit XML (`<testcase name>` + `<failure>`/`<error>` ⇒ fail; `<skipped>` ⇒
10
+ * omitted) and a simple JSON form (array, or `{results:[…]}`, of `{locator|name, result|status}`).
11
+ *
12
+ * Only an EXPLICIT pass/fail produces a run; an unknown/skipped/errored/empty-locator entry is
13
+ * OMITTED (never recorded as a pass), so an inconclusive report can never make acceptance green.
14
+ */
15
+ export declare function parseRunReport(content: string, format: RunReportFormat): RunReportEntry[];
16
+ //# sourceMappingURL=ingest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../../src/accept/ingest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAEvD,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,CAAA;AAE9C,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,SAAS,CAAA;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,cAAc,EAAE,CAEzF"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Parse a test report into `{locator, result}` entries (SPEC §6 `accept run --from`). Minimal,
3
+ * dependency-free: JUnit XML (`<testcase name>` + `<failure>`/`<error>` ⇒ fail; `<skipped>` ⇒
4
+ * omitted) and a simple JSON form (array, or `{results:[…]}`, of `{locator|name, result|status}`).
5
+ *
6
+ * Only an EXPLICIT pass/fail produces a run; an unknown/skipped/errored/empty-locator entry is
7
+ * OMITTED (never recorded as a pass), so an inconclusive report can never make acceptance green.
8
+ */
9
+ export function parseRunReport(content, format) {
10
+ return format === 'junit' ? parseJunit(content) : parseJson(content);
11
+ }
12
+ function parseJunit(content) {
13
+ const entries = [];
14
+ const testcase = /<testcase\b([^>]*?)(\/>|>([\s\S]*?)<\/testcase>)/g;
15
+ let match;
16
+ while ((match = testcase.exec(content)) !== null) {
17
+ const attrs = match[1] ?? '';
18
+ // Strip CDATA so XML-like text inside a body cannot be misread as a <failure>/<error> element.
19
+ const body = (match[3] ?? '').replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, '');
20
+ if (/<skipped\b/.test(body))
21
+ continue; // a skipped test is not a run
22
+ const name = /\bname\s*=\s*"([^"]*)"/.exec(attrs);
23
+ const locator = name?.[1] ?? '';
24
+ if (locator === '')
25
+ continue;
26
+ const failed = /<(failure|error)\b/.test(body);
27
+ entries.push({ locator, result: failed ? 'fail' : 'pass' });
28
+ }
29
+ return entries;
30
+ }
31
+ function normalizeResult(status) {
32
+ if (status === 'pass' || status === 'passed')
33
+ return 'pass';
34
+ if (status === 'fail' || status === 'failed')
35
+ return 'fail';
36
+ return undefined; // unknown / skipped / errored / missing → omit
37
+ }
38
+ function parseJson(content) {
39
+ const data = JSON.parse(content);
40
+ const list = Array.isArray(data)
41
+ ? data
42
+ : typeof data === 'object' &&
43
+ data !== null &&
44
+ Array.isArray(data.results)
45
+ ? data.results
46
+ : [];
47
+ const entries = [];
48
+ for (const raw of list) {
49
+ const r = raw;
50
+ const locator = String(r.locator ?? r.name ?? '');
51
+ if (locator === '')
52
+ continue;
53
+ const result = normalizeResult(r.result ?? r.status);
54
+ if (result === undefined)
55
+ continue;
56
+ entries.push({ locator, result });
57
+ }
58
+ return entries;
59
+ }
60
+ //# sourceMappingURL=ingest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest.js","sourceRoot":"","sources":["../../src/accept/ingest.ts"],"names":[],"mappings":"AASA;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,MAAuB;IACrE,OAAO,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;AACtE,CAAC;AAED,SAAS,UAAU,CAAC,OAAe;IACjC,MAAM,OAAO,GAAqB,EAAE,CAAA;IACpC,MAAM,QAAQ,GAAG,mDAAmD,CAAA;IACpE,IAAI,KAA6B,CAAA;IACjC,OAAO,CAAC,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACjD,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAC5B,+FAA+F;QAC/F,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,2BAA2B,EAAE,EAAE,CAAC,CAAA;QACtE,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAQ,CAAC,8BAA8B;QACpE,MAAM,IAAI,GAAG,wBAAwB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACjD,MAAM,OAAO,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAC/B,IAAI,OAAO,KAAK,EAAE;YAAE,SAAQ;QAC5B,MAAM,MAAM,GAAG,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC9C,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAA;IAC7D,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,SAAS,eAAe,CAAC,MAAe;IACtC,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAA;IAC3D,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAA;IAC3D,OAAO,SAAS,CAAA,CAAC,+CAA+C;AAClE,CAAC;AAED,SAAS,SAAS,CAAC,OAAe;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAY,CAAA;IAC3C,MAAM,IAAI,GAAc,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QACzC,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,OAAO,IAAI,KAAK,QAAQ;YACtB,IAAI,KAAK,IAAI;YACb,KAAK,CAAC,OAAO,CAAE,IAA8B,CAAC,OAAO,CAAC;YACxD,CAAC,CAAE,IAA+B,CAAC,OAAO;YAC1C,CAAC,CAAC,EAAE,CAAA;IACR,MAAM,OAAO,GAAqB,EAAE,CAAA;IACpC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,CAAC,GAAG,GAAgF,CAAA;QAC1F,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAA;QACjD,IAAI,OAAO,KAAK,EAAE;YAAE,SAAQ;QAC5B,MAAM,MAAM,GAAG,eAAe,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC,CAAA;QACpD,IAAI,MAAM,KAAK,SAAS;YAAE,SAAQ;QAClC,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAA;IACnC,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { AcceptanceStatus, CriterionStatus, EvidenceState } from '../model/acceptance.js';
2
+ import type { ItemId } from '../model/item.js';
3
+ import type { State } from '../state/fold.js';
4
+ export declare function evidenceForCriterion(state: State, criterionId: string): EvidenceState[];
5
+ /**
6
+ * Per-criterion status — the SPEC §2.4 ordered cascade over the criterion's evidence
7
+ * (latest run per evidence). A live `fail` overrides a waiver (A6).
8
+ */
9
+ export declare function criterionStatus(state: State, criterionId: string, baselineCommit: string): CriterionStatus;
10
+ /**
11
+ * Per-item acceptance — the SPEC §2.4 ordered cascade over the item's criteria.
12
+ * Zero criteria ⇒ `unknown`. (Decisions have no criteria — `n/a` is structural.)
13
+ */
14
+ export declare function acceptanceStatus(state: State, itemId: ItemId, baselineCommit: string): AcceptanceStatus;
15
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/accept/status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC9F,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAE7C,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CAEvF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,KAAK,EACZ,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,GACrB,eAAe,CAUjB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GACrB,gBAAgB,CAUlB"}
@@ -0,0 +1,44 @@
1
+ export function evidenceForCriterion(state, criterionId) {
2
+ return [...state.evidence.values()].filter((e) => e.criterionId === criterionId);
3
+ }
4
+ /**
5
+ * Per-criterion status — the SPEC §2.4 ordered cascade over the criterion's evidence
6
+ * (latest run per evidence). A live `fail` overrides a waiver (A6).
7
+ */
8
+ export function criterionStatus(state, criterionId, baselineCommit) {
9
+ const criterion = state.criteria.get(criterionId);
10
+ const evidence = evidenceForCriterion(state, criterionId);
11
+ if (evidence.some((e) => e.latestRun?.result === 'fail'))
12
+ return 'fail'; // (1) live fail wins
13
+ if (criterion?.waiver !== undefined)
14
+ return 'waived'; // (2) waiver
15
+ if (evidence.length === 0)
16
+ return 'unknown'; // no evidence and no waiver
17
+ if (evidence.some((e) => e.latestRun === undefined))
18
+ return 'unknown'; // (3) any evidence no run
19
+ if (evidence.some((e) => e.latestRun.commit !== baselineCommit))
20
+ return 'stale'; // (4) not at baseline
21
+ return 'pass'; // (5) all evidence latest = pass at baseline
22
+ }
23
+ /**
24
+ * Per-item acceptance — the SPEC §2.4 ordered cascade over the item's criteria.
25
+ * Zero criteria ⇒ `unknown`. (Decisions have no criteria — `n/a` is structural.)
26
+ */
27
+ export function acceptanceStatus(state, itemId, baselineCommit) {
28
+ if (state.decisions.has(itemId))
29
+ return 'n/a'; // a Decision has no acceptance axis (SPEC §2.4/§2.5)
30
+ const criteria = [...state.criteria.values()].filter((c) => c.itemId === itemId);
31
+ if (criteria.length === 0)
32
+ return 'unknown';
33
+ const statuses = criteria.map((c) => criterionStatus(state, c.id, baselineCommit));
34
+ if (statuses.includes('fail'))
35
+ return 'fail';
36
+ if (statuses.includes('unknown'))
37
+ return 'unknown';
38
+ if (statuses.includes('stale'))
39
+ return 'stale';
40
+ if (statuses.includes('waived'))
41
+ return 'waived';
42
+ return 'pass';
43
+ }
44
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/accept/status.ts"],"names":[],"mappings":"AAIA,MAAM,UAAU,oBAAoB,CAAC,KAAY,EAAE,WAAmB;IACpE,OAAO,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,KAAK,WAAW,CAAC,CAAA;AAClF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAY,EACZ,WAAmB,EACnB,cAAsB;IAEtB,MAAM,SAAS,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IACjD,MAAM,QAAQ,GAAG,oBAAoB,CAAC,KAAK,EAAE,WAAW,CAAC,CAAA;IAEzD,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;QAAE,OAAO,MAAM,CAAA,CAAC,qBAAqB;IAC7F,IAAI,SAAS,EAAE,MAAM,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAA,CAAC,aAAa;IAClE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA,CAAC,4BAA4B;IACxE,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC;QAAE,OAAO,SAAS,CAAA,CAAC,0BAA0B;IAChG,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAU,CAAC,MAAM,KAAK,cAAc,CAAC;QAAE,OAAO,OAAO,CAAA,CAAC,sBAAsB;IACvG,OAAO,MAAM,CAAA,CAAC,6CAA6C;AAC7D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAAY,EACZ,MAAc,EACd,cAAsB;IAEtB,IAAI,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAA,CAAC,qDAAqD;IACnG,MAAM,QAAQ,GAAG,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAA;IAChF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAC3C,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC,CAAA;IAClF,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAA;IAC5C,IAAI,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAA;IAClD,IAAI,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAA;IAC9C,IAAI,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAA;IAChD,OAAO,MAAM,CAAA;AACf,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { parseBranch, slugify, type ParsedBranch, type ParsedLot, type ParsedUat, } from './parse.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/branch/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EACX,OAAO,EACP,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,SAAS,GACf,MAAM,YAAY,CAAA"}
@@ -0,0 +1,3 @@
1
+ // BRANCH.md import/annotate: parse BRANCH_TEMPLATE sections, derive Items, read-only on BRANCH.md (SPEC §5).
2
+ export { parseBranch, slugify, } from './parse.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/branch/index.ts"],"names":[],"mappings":"AAAA,6GAA6G;AAC7G,OAAO,EACL,WAAW,EACX,OAAO,GAIR,MAAM,YAAY,CAAA"}
@@ -0,0 +1,31 @@
1
+ export interface ParsedUat {
2
+ uatSlug: string;
3
+ statement: string;
4
+ passed: boolean;
5
+ }
6
+ export interface ParsedLot {
7
+ lotSlug: string;
8
+ title: string;
9
+ done: boolean;
10
+ uat: ParsedUat[];
11
+ }
12
+ export interface ParsedBranch {
13
+ branchSlug: string;
14
+ feature: {
15
+ title: string;
16
+ body: string;
17
+ };
18
+ lots: ParsedLot[];
19
+ }
20
+ /** Stable kebab slug from a title fragment (NFKD-folded, non-alphanumerics → `-`). */
21
+ export declare function slugify(s: string): string;
22
+ /**
23
+ * Parse the stable `BRANCH_TEMPLATE` sections (SPEC §5): `# Feature:` → title; `## Objective` +
24
+ * `## Scope` → body; `## Plan / Todo (lot-based)` → lots (`- [x] **Lot N — slug**`, any checkbox
25
+ * marker); UAT checkboxes nested under a lot (text contains "UAT") → criteria. Gate sub-checkboxes
26
+ * are ignored. Tolerant: unknown sections are skipped; never throws on extra prose.
27
+ */
28
+ export declare function parseBranch(content: string, opts?: {
29
+ fileSlug?: string;
30
+ }): ParsedBranch;
31
+ //# sourceMappingURL=parse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/branch/parse.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,OAAO,CAAA;IACb,GAAG,EAAE,SAAS,EAAE,CAAA;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACxC,IAAI,EAAE,SAAS,EAAE,CAAA;CAClB;AAED,sFAAsF;AACtF,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAMzC;AA8BD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,YAAY,CA2E3F"}
@@ -0,0 +1,116 @@
1
+ /** Stable kebab slug from a title fragment (NFKD-folded, non-alphanumerics → `-`). */
2
+ export function slugify(s) {
3
+ return s
4
+ .normalize('NFKD')
5
+ .toLowerCase()
6
+ .replace(/[^a-z0-9]+/g, '-')
7
+ .replace(/^-+|-+$/g, '');
8
+ }
9
+ const FEATURE = /^#\s*Feature:\s*(.+?)\s*$/;
10
+ const HEADING = /^##\s+(.+?)\s*$/;
11
+ // A top-level lot line: `- [x] **<bold>** <trailing>` — marker is any single char (incl. `~`/space).
12
+ const LOT = /^-\s*\[([^\]])\]\s*\*\*(.+?)\*\*\s*(.*)$/;
13
+ // The bold label: `Lot <id>` with an OPTIONAL spaced-dash title (so the hyphen in `N-2` is NOT
14
+ // taken as the separator). `**Lot N-2** UAT` → id `N-2`, title falls back to the trailing `UAT`.
15
+ const LOT_LABEL = /^Lot\s+(\S+)(?:\s+[—–-]\s+(.+))?$/;
16
+ // A nested checkbox (gate or UAT): leading indentation required.
17
+ const NESTED = /^\s+-\s*\[([^\]])\]\s*(.+?)\s*$/;
18
+ // A BR id: `BR` + optional hyphen + a DIGIT (so "BRANCH" is not mistaken for an id).
19
+ const BR_ID = /\bBR-?\d[A-Za-z0-9]*/;
20
+ function lotTitle(boldText, trailing) {
21
+ const m = LOT_LABEL.exec(boldText.trim());
22
+ if (!m)
23
+ return undefined;
24
+ const dashTitle = m[2]?.trim();
25
+ if (dashTitle)
26
+ return dashTitle;
27
+ const tail = trailing.trim();
28
+ return tail.length > 0 ? tail : m[1]; // fall back to trailing text, else the id
29
+ }
30
+ function deriveBranchSlug(content, title, fileSlug) {
31
+ const brId = BR_ID.exec(content)?.[0];
32
+ if (brId)
33
+ return slugify(brId);
34
+ if (fileSlug)
35
+ return slugify(fileSlug);
36
+ return slugify(title);
37
+ }
38
+ /**
39
+ * Parse the stable `BRANCH_TEMPLATE` sections (SPEC §5): `# Feature:` → title; `## Objective` +
40
+ * `## Scope` → body; `## Plan / Todo (lot-based)` → lots (`- [x] **Lot N — slug**`, any checkbox
41
+ * marker); UAT checkboxes nested under a lot (text contains "UAT") → criteria. Gate sub-checkboxes
42
+ * are ignored. Tolerant: unknown sections are skipped; never throws on extra prose.
43
+ */
44
+ export function parseBranch(content, opts = {}) {
45
+ const lines = content.split('\n');
46
+ let title = '';
47
+ const objective = [];
48
+ const scope = [];
49
+ const lots = [];
50
+ let section = 'none';
51
+ let currentLot;
52
+ for (const line of lines) {
53
+ const feature = FEATURE.exec(line);
54
+ if (feature) {
55
+ title = feature[1];
56
+ section = 'none';
57
+ continue;
58
+ }
59
+ const heading = HEADING.exec(line);
60
+ if (heading) {
61
+ const name = heading[1].toLowerCase();
62
+ section = name.startsWith('objective')
63
+ ? 'objective'
64
+ : name.startsWith('scope')
65
+ ? 'scope'
66
+ : name.startsWith('plan')
67
+ ? 'plan'
68
+ : 'other';
69
+ currentLot = undefined;
70
+ continue;
71
+ }
72
+ if (section === 'objective') {
73
+ if (line.trim())
74
+ objective.push(line.trim());
75
+ continue;
76
+ }
77
+ if (section === 'scope') {
78
+ if (line.trim())
79
+ scope.push(line.trim());
80
+ continue;
81
+ }
82
+ if (section === 'plan') {
83
+ const lot = LOT.exec(line);
84
+ if (lot) {
85
+ const derived = lotTitle(lot[2], lot[3] ?? '');
86
+ if (derived !== undefined) {
87
+ currentLot = {
88
+ lotSlug: slugify(derived),
89
+ title: derived,
90
+ done: lot[1].toLowerCase() === 'x',
91
+ uat: [],
92
+ };
93
+ lots.push(currentLot);
94
+ }
95
+ continue;
96
+ }
97
+ const nested = NESTED.exec(line);
98
+ if (nested && currentLot && /\buat\b/i.test(nested[2])) {
99
+ const statement = nested[2];
100
+ currentLot.uat.push({
101
+ uatSlug: slugify(statement),
102
+ statement,
103
+ passed: nested[1].toLowerCase() === 'x',
104
+ });
105
+ }
106
+ // gate sub-checkboxes (no "UAT") are ignored (SPEC §5)
107
+ }
108
+ }
109
+ const body = [objective.join('\n'), scope.join('\n')].filter(Boolean).join('\n\n');
110
+ return {
111
+ branchSlug: deriveBranchSlug(content, title, opts.fileSlug),
112
+ feature: { title, body },
113
+ lots,
114
+ };
115
+ }
116
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/branch/parse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { State } from '../state/fold.js';
2
+ export interface DesyncFinding {
3
+ kind: 'desync';
4
+ itemId: string;
5
+ reason: string;
6
+ }
7
+ /**
8
+ * SPEC §4 round-trip / desync rule: when an Item's `body` references a markdown file (a single-line
9
+ * path ending in `.md`), that file MUST exist and its H1 title MUST match the Item title. A missing
10
+ * file or a title mismatch is a desync finding (MVP reports; it never auto-repairs). Inline-prose
11
+ * bodies (the common case, incl. BRANCH-imported items) are not file references and are skipped.
12
+ */
13
+ export declare function desyncFindings(state: State, cwd: string): DesyncFinding[];
14
+ //# sourceMappingURL=desync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"desync.d.ts","sourceRoot":"","sources":["../../src/cli/desync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAE7C,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,QAAQ,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,EAAE,CAuBzE"}
@@ -0,0 +1,35 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { isAbsolute, join } from 'node:path';
3
+ /**
4
+ * SPEC §4 round-trip / desync rule: when an Item's `body` references a markdown file (a single-line
5
+ * path ending in `.md`), that file MUST exist and its H1 title MUST match the Item title. A missing
6
+ * file or a title mismatch is a desync finding (MVP reports; it never auto-repairs). Inline-prose
7
+ * bodies (the common case, incl. BRANCH-imported items) are not file references and are skipped.
8
+ */
9
+ export function desyncFindings(state, cwd) {
10
+ const findings = [];
11
+ for (const item of state.items.values()) {
12
+ const ref = item.body?.trim();
13
+ if (ref === undefined || !/^[^\n]+\.md$/.test(ref))
14
+ continue;
15
+ const path = isAbsolute(ref) ? ref : join(cwd, ref);
16
+ if (!existsSync(path)) {
17
+ findings.push({ kind: 'desync', itemId: item.id, reason: `referenced markdown missing: ${ref}` });
18
+ continue;
19
+ }
20
+ const h1 = /^#\s+(.+?)\s*$/m.exec(readFileSync(path, 'utf8'))?.[1];
21
+ if (h1 === undefined) {
22
+ // SPEC §4 requires the H1 to MATCH the title; a file with no H1 cannot match.
23
+ findings.push({ kind: 'desync', itemId: item.id, reason: `referenced markdown has no H1: ${ref}` });
24
+ }
25
+ else if (h1 !== item.title) {
26
+ findings.push({
27
+ kind: 'desync',
28
+ itemId: item.id,
29
+ reason: `H1 "${h1}" != item title "${item.title}" (${ref})`,
30
+ });
31
+ }
32
+ }
33
+ return findings;
34
+ }
35
+ //# sourceMappingURL=desync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"desync.js","sourceRoot":"","sources":["../../src/cli/desync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAU5C;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,KAAY,EAAE,GAAW;IACtD,MAAM,QAAQ,GAAoB,EAAE,CAAA;IACpC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAA;QAC7B,IAAI,GAAG,KAAK,SAAS,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,SAAQ;QAC5D,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QACnD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,gCAAgC,GAAG,EAAE,EAAE,CAAC,CAAA;YACjG,SAAQ;QACV,CAAC;QACD,MAAM,EAAE,GAAG,iBAAiB,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;QAClE,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;YACrB,8EAA8E;YAC9E,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,kCAAkC,GAAG,EAAE,EAAE,CAAC,CAAA;QACrG,CAAC;aAAM,IAAI,EAAE,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;YAC7B,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,QAAQ;gBACd,MAAM,EAAE,IAAI,CAAC,EAAE;gBACf,MAAM,EAAE,OAAO,EAAE,oBAAoB,IAAI,CAAC,KAAK,MAAM,GAAG,GAAG;aAC5D,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC"}
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ export interface CliIO {
3
+ cwd: string;
4
+ out: (s: string) => void;
5
+ err: (s: string) => void;
6
+ }
7
+ export declare function runCli(argv: string[], io: CliIO): number;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAkBA,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;CACzB;AAmHD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,GAAG,MAAM,CAmCxD"}