lazycodex-ai 5.0.0-beta.89 → 5.0.0-beta.90

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 (159) hide show
  1. package/README.ja.md +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/README.ru.md +1 -1
  5. package/README.zh-cn.md +1 -1
  6. package/dist/cli/config-manager/refresh-opencode-plugin-sandbox.d.ts +31 -0
  7. package/dist/cli/doctor/checks/tui-plugin-config.d.ts +11 -0
  8. package/dist/cli/index.js +1661 -859
  9. package/dist/cli/install-native/index.d.ts +9 -1
  10. package/dist/cli/install-native/legacy-omo-bin.d.ts +27 -0
  11. package/dist/cli/install-native/offer-native-setup.d.ts +31 -0
  12. package/dist/cli/install-native/omo-bin-test-fixtures.d.ts +35 -0
  13. package/dist/cli/install-native/plan.d.ts +8 -0
  14. package/dist/cli/install-native/repair-legacy-omo-bin.d.ts +26 -0
  15. package/dist/cli/install-native/run-native-install.d.ts +11 -1
  16. package/dist/cli/install-native/verify-omo-command.d.ts +22 -0
  17. package/dist/cli/native-edition-hint.d.ts +2 -2
  18. package/dist/cli-node/index.js +1661 -859
  19. package/package.json +1 -1
  20. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  21. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  22. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  23. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  24. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  25. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  26. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  27. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  28. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  29. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  30. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  31. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  32. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +5 -1
  33. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-6.md +5 -1
  34. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  35. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  36. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  37. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  38. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  39. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  40. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +7 -3
  41. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  42. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  43. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  44. package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
  45. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  46. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  47. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +7 -3
  48. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  49. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  50. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  51. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  52. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  53. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  54. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  55. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  56. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  57. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  58. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  59. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  60. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  61. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  62. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  63. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  64. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  65. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  66. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  67. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  68. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  69. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  70. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  71. package/packages/omo-codex/plugin/package-lock.json +12 -12
  72. package/packages/omo-codex/plugin/package.json +1 -1
  73. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +2 -2
  74. package/packages/omo-codex/plugin/skills/browser/SKILL.md +8 -1
  75. package/packages/omo-codex/plugin/skills/browser/references/install.md +34 -5
  76. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/index.js +2 -2
  77. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +3 -3
  78. package/packages/omo-codex/plugin/skills/browser/scripts/browser-doctor.mjs +14 -3
  79. package/packages/omo-codex/plugin/skills/browser/scripts/browser-install.mjs +13 -1
  80. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +7 -3
  81. package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +63 -0
  82. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +37 -25
  83. package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +187 -0
  84. package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +66 -0
  85. package/packages/omo-codex/plugin/skills/ulw-research/scripts/cli-support.mjs +102 -0
  86. package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +162 -0
  87. package/packages/omo-codex/plugin/skills/ulw-research/scripts/css-lite.mjs +204 -0
  88. package/packages/omo-codex/plugin/skills/ulw-research/scripts/design-spec.mjs +161 -0
  89. package/packages/omo-codex/plugin/skills/ulw-research/scripts/entities.mjs +39 -0
  90. package/packages/omo-codex/plugin/skills/ulw-research/scripts/entry-guard.mjs +21 -0
  91. package/packages/omo-codex/plugin/skills/ulw-research/scripts/format-extract-css.mjs +122 -0
  92. package/packages/omo-codex/plugin/skills/ulw-research/scripts/format-extract.mjs +229 -0
  93. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-figures.mjs +173 -0
  94. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-layout.mjs +146 -0
  95. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +167 -0
  96. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-structure.mjs +118 -0
  97. package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-text.mjs +190 -0
  98. package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-lite.mjs +239 -0
  99. package/packages/omo-codex/plugin/skills/ulw-research/scripts/layout-probe.mjs +83 -0
  100. package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +250 -0
  101. package/packages/omo-codex/plugin/skills/ulw-research/scripts/repair-tracker.mjs +171 -0
  102. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +214 -0
  103. package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools.mjs +61 -0
  104. package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +3 -1
  105. package/packages/omo-codex/plugin/test/sync-skills-codex-compatibility.test.mjs +11 -0
  106. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +2 -2
  107. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +20 -0
  108. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
  109. package/packages/prompts-core/prompts/ultrawork/codex.md +7 -3
  110. package/packages/shared-skills/skills/browser/SKILL.md +8 -1
  111. package/packages/shared-skills/skills/browser/references/install.md +34 -5
  112. package/packages/shared-skills/skills/browser/runtime/omowright/index.js +2 -2
  113. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +3 -3
  114. package/packages/shared-skills/skills/browser/scripts/browser-doctor.mjs +14 -3
  115. package/packages/shared-skills/skills/browser/scripts/browser-install.mjs +13 -1
  116. package/packages/shared-skills/skills/ulw-research/AGENTS.md +63 -0
  117. package/packages/shared-skills/skills/ulw-research/SKILL.md +38 -26
  118. package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +187 -0
  119. package/packages/shared-skills/skills/ulw-research/references/report-gates.md +66 -0
  120. package/packages/shared-skills/skills/ulw-research/scripts/cli-support.mjs +102 -0
  121. package/packages/shared-skills/skills/ulw-research/scripts/cli-support.test.ts +88 -0
  122. package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +162 -0
  123. package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +177 -0
  124. package/packages/shared-skills/skills/ulw-research/scripts/css-lite.mjs +204 -0
  125. package/packages/shared-skills/skills/ulw-research/scripts/css-lite.test.ts +159 -0
  126. package/packages/shared-skills/skills/ulw-research/scripts/design-spec.mjs +161 -0
  127. package/packages/shared-skills/skills/ulw-research/scripts/design-spec.test.ts +162 -0
  128. package/packages/shared-skills/skills/ulw-research/scripts/entities.mjs +39 -0
  129. package/packages/shared-skills/skills/ulw-research/scripts/entry-guard.mjs +21 -0
  130. package/packages/shared-skills/skills/ulw-research/scripts/format-extract-css.mjs +122 -0
  131. package/packages/shared-skills/skills/ulw-research/scripts/format-extract.mjs +229 -0
  132. package/packages/shared-skills/skills/ulw-research/scripts/format-extract.test.ts +243 -0
  133. package/packages/shared-skills/skills/ulw-research/scripts/gates-figures.mjs +173 -0
  134. package/packages/shared-skills/skills/ulw-research/scripts/gates-layout.mjs +146 -0
  135. package/packages/shared-skills/skills/ulw-research/scripts/gates-layout.test.ts +236 -0
  136. package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +167 -0
  137. package/packages/shared-skills/skills/ulw-research/scripts/gates-static.test.ts +265 -0
  138. package/packages/shared-skills/skills/ulw-research/scripts/gates-structure.mjs +118 -0
  139. package/packages/shared-skills/skills/ulw-research/scripts/gates-text.mjs +190 -0
  140. package/packages/shared-skills/skills/ulw-research/scripts/glossary-vocabulary.test.ts +43 -0
  141. package/packages/shared-skills/skills/ulw-research/scripts/html-lite.mjs +239 -0
  142. package/packages/shared-skills/skills/ulw-research/scripts/html-lite.test.ts +191 -0
  143. package/packages/shared-skills/skills/ulw-research/scripts/layout-probe.mjs +83 -0
  144. package/packages/shared-skills/skills/ulw-research/scripts/layout-probe.test.ts +89 -0
  145. package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +250 -0
  146. package/packages/shared-skills/skills/ulw-research/scripts/outcome.test.ts +281 -0
  147. package/packages/shared-skills/skills/ulw-research/scripts/repair-tracker.mjs +171 -0
  148. package/packages/shared-skills/skills/ulw-research/scripts/repair-tracker.test.ts +246 -0
  149. package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +214 -0
  150. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.mjs +61 -0
  151. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +213 -0
  152. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/assets/figure-1.svg +1 -0
  153. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/bad-report.html +44 -0
  154. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/boxes.json +90 -0
  155. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/design-spec.md +29 -0
  156. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/good-report.html +57 -0
  157. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/reference-report.html +86 -0
  158. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/reference.md +23 -0
  159. package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +3 -1
@@ -0,0 +1,187 @@
1
+ # ulw-research - deliverable phase contract
2
+
3
+ Read this before the brief is written and again before Phase 6. It replaces the old blocking format question with a contract that never stalls collection: the deliverable is derived from the request and its destination, the requester is asked only what is genuinely missing, and every promise made about the output is tracked in a manifest until it is delivered, skipped, or blocked with a reason.
4
+
5
+ Every helper named here is one command of the dispatcher:
6
+
7
+ ```bash
8
+ node "$SKILL_DIR/scripts/report-tools.mjs" <command> ...
9
+ ```
10
+
11
+ `$SKILL_DIR` is the directory of the skill's `SKILL.md`. The helpers are plain `.mjs` files with no dependencies and run under `node` or `bun` on macOS, Linux, and Windows. They never launch a browser. Exit codes: `0` pass, `1` semantic failure (blockers found, a failed verify, a blocked repair), `2` usage or IO error. JSON goes to stdout and a one-line summary to stderr. Always pass `--session-dir "$SESSION_DIR"` explicitly; `SESSION_DIR` in the environment is only a fallback.
12
+
13
+ ## 1. Lanes
14
+
15
+ The brief names exactly one lane. The lane decides how much the requester is asked, what the design spec starts from, and how strict the gates are.
16
+
17
+ | Lane | Definition | How to say it to the requester |
18
+ | --- | --- | --- |
19
+ | `template-strict` | The requester pointed at a document (a prior report, a page, a house template). Its structure, tokens, citation style, and figure standard are copied mechanically and not reinterpreted. | "I will match the document you pointed at: same structure, colors, fonts, and citation style." |
20
+ | `template-vibe` | The requester named a register or feeling ("like a consulting deck", "a clean blog post") without a document to copy. The design spec is written from the defaults in section 6 and the register. | "I will write it in that style using the default report design; point me at an example if you want an exact match." |
21
+ | `no-format` | Nothing about the shape was said and the destination implies it (a chat answer, a thread post, a quick note). No interview about format. | "I will answer in the shape this destination expects." |
22
+ | `edit-existing` | A complete deliverable for this question already exists (section 2). The run edits it in place. It never regenerates it. | "I will update the existing report rather than write a new one." |
23
+
24
+ ## 2. Deliverable state
25
+
26
+ Before the brief is written, decide the state of any deliverable the request refers to:
27
+
28
+ ```bash
29
+ node "$SKILL_DIR/scripts/report-tools.mjs" outcome state --deliverable <path> --session-dir "$SESSION_DIR"
30
+ ```
31
+
32
+ | State | Meaning | What it opens |
33
+ | --- | --- | --- |
34
+ | `none` | No file, or an empty one. | A fresh run in the lane the request implies. |
35
+ | `partial` | The file carries a `STATUS: draft` line in its first 5 lines, or its manifest still has a `pending` row. | Resume the on-disk skeleton: keep the sections that exist, fill the missing ones, and never start over. |
36
+ | `complete` | The manifest says every promised row is resolved and the file is present and non-empty. | `edit-existing` only. A complete report is never regenerated from scratch. |
37
+
38
+ ## 3. Destination-derived defaults
39
+
40
+ The request and its destination usually answer the format question already. Derive the defaults first; the interview (section 4) only covers what this table leaves open.
41
+
42
+ | Request or destination signal | Default format set | Lane | QA tier |
43
+ | --- | --- | --- | --- |
44
+ | A question answered in chat, with no document asked for | `post` (the answer itself, Markdown) | `no-format` | light |
45
+ | A thread, channel, or messenger post | `post` | `no-format` | light |
46
+ | A web page URL as the destination, or "blog", "page" | `html` | `template-vibe` | full |
47
+ | The words "PDF", "DOCX", "slides", or "LaTeX" in the request | exactly the named formats (`pdf`, `docx`, `slides`, `latex`) | `template-vibe` | full |
48
+ | "Report", "document", "write-up" with no format word and no destination | `pdf` + `docx` | `template-vibe` | full |
49
+ | A pointed-at document (a file, a URL, a prior report) | the reference's own format | `template-strict` | full |
50
+ | A prior report of the same kind in this workspace, not pointed at | that report's format, offered as the Q1 default | `template-vibe` | full |
51
+ | A complete existing deliverable for this question (section 2) | its existing format | `edit-existing` | the original's tier |
52
+
53
+ QA tiers: **light** runs the static gates and one render check of the answer. **Full** runs the static gates, the layout gates, visual QA of every page, and the proofread gate (section 7).
54
+
55
+ ## 4. The empty-info interview
56
+
57
+ The interview has exactly three questions, asked together through the harness question tool without waiting for the answer. A question is **skipped** when the request, the destination, a pointed-at document, or a declared memory episode already answers it; when all three are answered, nothing is asked.
58
+
59
+ - **Q1 - destination and format.** Options in this order: the memory hit (the choice recorded for the most similar prior context, section 5) when there is one; the derived default from section 3; "describe the format in your own words" (free text, which becomes the brief for the design spec); "don't care, you decide" (resolves to the derived default).
60
+ - **Q2 - audience and length.** Who reads it, and a length band: `short` (a page or a post), `standard` (a report of about 5 to 12 pages), or `deep` (a full dossier). The derived default is first.
61
+ - **Q3 - template lineage.** The pointed-at document, the last report of the same kind, or the clean analyst register (section 6). The derived default is first.
62
+
63
+ Non-blocking rule: ask, do not wait, and start collection. Write `brief.md` at once with every field and who answered it; the unanswered fields carry their defaults. A late answer is folded in until the assembly lane starts; after that it becomes a re-render request, handled like any other repair.
64
+
65
+ Recorded fields, under `## Deliverable` in `brief.md`:
66
+
67
+ ```text
68
+ lane: template-vibe
69
+ state: none
70
+ formats: pdf, docx (answered_by: request)
71
+ destination: internal review document (answered_by: default)
72
+ audience: engineering leads, standard (answered_by: user)
73
+ template: clean analyst register (answered_by: default)
74
+ format description: none
75
+ ```
76
+
77
+ `answered_by` is one of `user`, `default`, `request`. Run `outcome init` (section 9) in the same step, with the formats as the promised list.
78
+
79
+ ## 5. Report-format memory episodes
80
+
81
+ When the memory tool is present, the requester's report-format choices are kept as an append-only episode log, so a repeat requester is not asked the same question twice. When no memory tool is present, skip this section silently.
82
+
83
+ - Pointer: `system/human/report-style.md`, projected every session. One line naming the log, plus the generalizations currently in force.
84
+ - Episode log: `reference/human-report-style.md`, one line per episode, newest last:
85
+
86
+ ```text
87
+ - [YYYY-MM-DD] <destination kind> · <audience> - <format and template chosen, one line> <!-- src: <session-id>; pattern: observed|declared; confidence: low|medium|high -->
88
+ ```
89
+
90
+ Qualification (write after delivery, only when one of these holds): `declared` - the requester stated the preference outright ("always PDF only"); `observed` - the requester made the same unsolicited choice on 2 or more runs (read the log first). Record format and template choices only, never report content, subjects, names, or findings. Append; never rewrite an existing line.
91
+
92
+ Read-time generalization (before the interview): a similar context is one with the same destination kind and audience. Consistent similar episodes supply Q1's first option; a `declared` similar episode skips the question it answers. One session never flips a default: a single `observed` episode that disagrees with the rest changes nothing.
93
+
94
+ Pruning: keep the log near 15 lines. When it grows past that, prune the oldest episodes that agree with the majority for their context first. Never prune a `declared` episode or a minority episode (the only one recording a different choice for its context). Update the pointer in the same memory commit when a generalization changes.
95
+
96
+ ## 6. design-spec.md
97
+
98
+ Write `$SESSION_DIR/design-spec.md` the moment the deliverable fields are known, before any asset lane starts. Every asset and assembly lane receives it.
99
+
100
+ 1. When a document was pointed at, extract it first. Do not retype its stylesheet:
101
+ ```bash
102
+ node "$SKILL_DIR/scripts/report-tools.mjs" format-extract <reference.html|reference.md> --out "$SESSION_DIR/design-spec.md"
103
+ node "$SKILL_DIR/scripts/report-tools.mjs" format-extract <https://...> --from-url --out "$SESSION_DIR/design-spec.md"
104
+ ```
105
+ The extractor copies the CSS custom properties (light and dark), font stacks by role, the most-used colors, px breakpoints, the section skeleton, the heading numbering, the citation marker style, the lineage mode, and the figure containers, under the fixed sections `Extracted from`, `Tokens`, `Typography`, `Layout`, `Structure`, `Figures`, `Citations`, `Open questions`. A field it cannot determine reads `TODO: ask`. A PDF reference is unsupported: ask for its HTML or Markdown source.
106
+ 2. Fill every `TODO: ask` from the interview answers, then from the clean analyst register below. Never guess a value.
107
+ 3. Apply the binding figure standard and the defaults:
108
+ - Every figure sits in a fixed-size container with a caption; the image scales inside it with contain-fit, its aspect ratio preserved, never stretched, cropped, or spilling out. Every chart carries a title, axis labels, units, and value labels as real text, with deterministic ticks (round values computed from the data range, never left to a renderer's auto-scaling).
109
+ - One accent color over neutral background, text, and rule tones. Every color in prose, charts, and diagrams is a spec token.
110
+ - For web deliverables, HTML/CSS bars and inline SVG are preferred over image charts, and the page has responsive breakpoints so it reads on a phone, a tablet, and a desktop.
111
+ - Gothic (sans) type only, as a real embedded webfont for the report language; Korean text uses `word-break: keep-all; overflow-wrap: anywhere`.
112
+ - A closing section on how the report was made (method, sources, members and waves, the gates it passed, what verification overturned) and a numbered sources section with access dates.
113
+ 4. Declare the lineage mode on its own line in the spec:
114
+ - `Lineage: inline` (the analyst default): every unit-bearing number carries `MEASURED`, `ASSUMED`, or `DERIVED` inline or in a `data-lineage` attribute, or a `[S<n>]` citation in the same element. The gate reports an untagged number as a major defect.
115
+ - `Lineage: section` (web and blog register): sources are bracketed source-label links at sentence end (the report language's word for "source"), lineage distinctions live in the figure captions, and every section is cited. The gate reports an untagged number as an advisory minor defect; the section-citation gate carries the weight.
116
+
117
+ ## 7. Delivery gates, in order
118
+
119
+ Nothing reaches the requester until the gates for its QA tier pass, in this order. Each gate writes its status into the manifest with `outcome gate <gate> <pass|fail|not_run> --session-dir "$SESSION_DIR"`.
120
+
121
+ 1. **Static gates.** `check` on the rendered HTML:
122
+ ```bash
123
+ node "$SKILL_DIR/scripts/report-tools.mjs" check "$SESSION_DIR/report.html" --design-spec "$SESSION_DIR/design-spec.md" > "$SESSION_DIR/defects.json"
124
+ ```
125
+ It checks keep-all, palette tokens, emoji, em dashes in prose (en dashes outside numeric ranges are advisory), heading length and generic headings, unsourced numbers (section 6), figure containers and captions, chart SVG text, Korean serif fonts, sections without citations, the closing section, the sources section, broken local assets, and any `--require-section <regex>` the brief requires. Every code, severity, and fix is in [report-gates.md](report-gates.md).
126
+ 2. **Layout gates.** Print the probe, evaluate it in the rendered page through the browser skill's owned headless engine, save the returned object as `$SESSION_DIR/boxes.json`, and re-run `check` with `--layout "$SESSION_DIR/boxes.json"`. It adds overflow past the container, clipped text, scroll containers, distorted images, and overlapping siblings.
127
+ ```bash
128
+ node "$SKILL_DIR/scripts/report-tools.mjs" layout-probe --json # .source is one async expression
129
+ ```
130
+ 3. **Visual QA.** Render the artifact to images (PDF pages to PNG, the HTML in the browser skill's engine at desktop and phone widths) and look at the pixels for what a machine cannot see: composition, hierarchy, page breaks, figure legibility.
131
+ 4. **Proofread.** A dedicated writing lane proofreads the final text only (grammar, spelling, punctuation, terminology, native register); it never composes or rewrites content.
132
+
133
+ The light tier runs gate 1 and one render (recorded as `visual`) and records `layout` and `proofread` as `not_run`. The full tier runs all four.
134
+
135
+ ## 8. Bounded repair loop
136
+
137
+ After every gate run that produced defects, ask the tracker what to do next:
138
+
139
+ ```bash
140
+ node "$SKILL_DIR/scripts/report-tools.mjs" repair decide --state "$SESSION_DIR/repair-state.json" --defects "$SESSION_DIR/defects.json" --artifact-bytes <report bytes> --renders <rendered page count> --session-dir "$SESSION_DIR"
141
+ ```
142
+
143
+ - `action: repair`: fix the listed defects, re-render, re-run the gate, and ask again.
144
+ - `action: deliver`: stop repairing and deliver. `reason` is `clean` or the stop condition that fired; the residual defects are written into the manifest and listed in the closing briefing.
145
+ - `action: block` (exit `1`): do not deliver. Either a content-integrity defect remains (`missing_closing_section`, `missing_citation_section`, `broken_asset_reference`, `missing_required_section`, `missing_promised_deliverable`), or a stop fired without proof of a usable artifact (`--artifact-bytes` and `--renders`). Tell the requester what blocks delivery and record the affected rows as `failed` with the reason.
146
+
147
+ Defaults: 3 repair attempts, a plateau of 2 consecutive attempts that do not improve the (blocker, major, minor) counts compared in that order, and 15 minutes of wall clock from the first decision. A defect set that repeats an earlier attempt exactly stops the loop as an oscillation. When several conditions hold at once the reported reason follows `clean`, `blocking`, `oscillation`, `plateau`, `max_attempts`, `wall_clock`.
148
+
149
+ ## 9. Outcome manifest and closing briefing
150
+
151
+ `$SESSION_DIR/outcome.json` tracks every promise made about the output, so a promised format is never silently dropped.
152
+
153
+ ```bash
154
+ node "$SKILL_DIR/scripts/report-tools.mjs" outcome init --promised pdf,docx --lane template-vibe --session-dir "$SESSION_DIR"
155
+ node "$SKILL_DIR/scripts/report-tools.mjs" outcome set pdf delivered --path "$SESSION_DIR/report.pdf" --pages 12 --session-dir "$SESSION_DIR"
156
+ node "$SKILL_DIR/scripts/report-tools.mjs" outcome set docx blocked_capability --reason "no document converter in this session" --session-dir "$SESSION_DIR"
157
+ node "$SKILL_DIR/scripts/report-tools.mjs" outcome render "$SESSION_DIR/renders/page-01.png" --page 1 --session-dir "$SESSION_DIR"
158
+ node "$SKILL_DIR/scripts/report-tools.mjs" outcome verify --session-dir "$SESSION_DIR"
159
+ node "$SKILL_DIR/scripts/report-tools.mjs" outcome finish --session-dir "$SESSION_DIR"
160
+ node "$SKILL_DIR/scripts/report-tools.mjs" outcome briefing --session-dir "$SESSION_DIR"
161
+ ```
162
+
163
+ - `init` runs when the brief is written (section 4): one `pending` row per promised format (`pdf`, `docx`, `html`, `md`, `latex`, `slides`, `post`).
164
+ - `set` resolves a row: `delivered` (absolute path; bytes are read from disk; pages when paginated), `blocked_capability` (this session cannot produce it), `skipped` (the requester or the lane dropped it), or `failed`. Every status except `delivered` needs `--reason`.
165
+ - `verify` runs before delivery and exits `1` while a row is `pending`, a status lacks its reason, or a delivered file is missing or empty.
166
+ - `finish` stamps the finish time, the elapsed minutes from the session directory's start stamp, and the source and domain counts from `sources-ledger.md`.
167
+ - `briefing` prints the closing block from the manifest and the ledger: sources and distinct domains, elapsed minutes, every deliverable with its status, the gate results, the residual defects, and the repair summary. That text is the closing briefing, pasted as printed, never filled in from memory.
168
+
169
+ ## 10. Command reference
170
+
171
+ Invoke every command as `node "$SKILL_DIR/scripts/report-tools.mjs" <command>`. Exit codes for all of them: `0` pass, `1` semantic failure, `2` usage or IO error.
172
+
173
+ | Command | Flags | Exit `1` when |
174
+ | --- | --- | --- |
175
+ | `check <report.html>` | `--design-spec <spec>` (required), `--layout <boxes.json>`, `--require-section <regex>` (repeatable), `--max-defects N` | any blocker defect |
176
+ | `layout-probe` | `--cap N` (default 400), `--root <selector>` (default `main, article`), `--json` | never |
177
+ | `repair decide` | `--state <file>` and `--defects <json>` (required), `--artifact-bytes N`, `--renders N`, `--session-dir <dir>`, `--max-attempts N`, `--plateau-limit N`, `--wall-clock-ms N`, `--now <ISO>` | the decision is `block` |
178
+ | `outcome init` | `--promised <list>` (required), `--lane <lane>`, `--session-dir <dir>`, `--now <ISO>` | never |
179
+ | `outcome set <format> <status>` | `--path <file>`, `--pages N`, `--reason <text>`, `--session-dir <dir>`, `--now <ISO>` | never |
180
+ | `outcome gate <gate> <status>` | `--session-dir <dir>` | never |
181
+ | `outcome render <png>` | `--page N` (required), `--session-dir <dir>` | never |
182
+ | `outcome state` | `--deliverable <path>` (required), `--session-dir <dir>` | never |
183
+ | `outcome verify` | `--session-dir <dir>` | a promise is unresolved or a delivered file is missing or empty |
184
+ | `outcome finish` | `--ledger <file>`, `--session-dir <dir>`, `--now <ISO>` | never |
185
+ | `outcome briefing` | `--json`, `--ledger <file>`, `--session-dir <dir>`, `--now <ISO>` | never |
186
+ | `format-extract <reference>` | `--out <design-spec.md>` (required), `--from-url` | never (an unsupported reference exits `2`) |
187
+ | `--help` | `--json` | never |
@@ -0,0 +1,66 @@
1
+ # Report-Gates Defect Glossary
2
+
3
+ This reference catalogs every defect code the report gates (`static`, `layout`, `visual`, `proofread`) can raise, their severity and impact, and how to repair them.
4
+
5
+ ## Defect Codes by Category
6
+
7
+ ### Static Gate (Content & Structure)
8
+
9
+ | Code | Severity | Integrity | What It Detects | How to Fix |
10
+ |------|----------|-----------|-----------------|-----------|
11
+ | `korean_no_keep_all` | major | false | Text containing Hangul uses default word breaking, causing character-level wrapping and orphaned characters on mobile or narrow containers. | Add "word-break: keep-all; overflow-wrap: anywhere" to the body or the slide text container. |
12
+ | `palette_off_token` | minor | false | Color values are hardcoded literals (hex, rgb) instead of design-system tokens or are missing from the design spec palette. | Replace the literal color with a design-spec token (var(--...)) or add the color to the spec palette. |
13
+ | `emoji_in_prose` | major | false | Emoji characters appear in body text, headings, or figure captions. Emoji render at inconsistent sizes and lack semantic meaning. | Remove the emoji; use an inline SVG icon or plain text. |
14
+ | `em_dash_in_prose` | major | false | Em dashes (—) are used in body text for sentence breaks or parenthetical asides. Readability and accessibility suffer without a rewrite. | Rewrite the sentence with a comma, colon, or period instead of an em dash. |
15
+ | `en_dash_in_prose` | minor | false | En dashes (–) are used incorrectly outside of numeric ranges. Only numeric ranges (e.g., "pages 5–10") should use en dashes. | Use a hyphen for compounds; keep the en dash only between numbers. |
16
+ | `heading_too_long` | minor | false | A heading exceeds one visual line: 26 Korean characters (52 display width units) or 12 English words. | Shorten the heading to one line (26 Korean characters or 12 English words). |
17
+ | `heading_generic` | minor | false | A heading uses generic labels (e.g., "Introduction", "Background") without conveying the section's specific claim or finding. | Make the heading carry the section's claim instead of a generic label. |
18
+ | `unsourced_number` | major | false | A numeric value (percentage, count, duration, ratio) appears without a source attribution, measurement tag, or derivation chain. | Tag the number MEASURED / ASSUMED / DERIVED or cite its source in the same paragraph. |
19
+ | `chart_without_figure` | major | false | An image or chart lacks a figure wrapper with a caption. Uncaptioned charts isolate visuals from narrative flow. | Wrap the image or chart in a figure container with a caption. |
20
+ | `chart_svg_no_text` | major | false | An SVG chart or diagram lacks a title, axis labels, units, or value labels. Visuals cannot stand alone without text guidance. | Give the chart a title, axis labels, units, and value labels as SVG text. |
21
+ | `chart_svg_missing_labels` | minor | false | An SVG chart has some labels (e.g., title) but is missing others (e.g., axis ticks or unit). | Add the missing title, axis ticks, or unit label to the chart. |
22
+ | `korean_serif_font` | major | false | A serif or brush Korean font family is applied to Korean text. The design spec mandates gothic (sans) Korean typefaces. | Use a gothic (sans) Korean family; serif and brush families are banned by the design spec. |
23
+ | `section_without_citation` | major | false | A major section or subsection contains no in-text citations or reference marks. Source attribution is missing. | Cite at least one source inside this section or merge it into a cited one. |
24
+ | `missing_closing_section` | blocker | true | The report lacks a closing section explaining the methodology, sources, and gate outcomes. Delivery is incomplete without narrative closure. | Add the closing section that explains how the report was made (method, sources, gates). |
25
+ | `missing_citation_section` | blocker | true | The report has no sources, references, or citation definitions (e.g., [S1], [S2]). Attribution is impossible. | Add a sources or references section, or [S<n>] definition lines. |
26
+ | `broken_asset_reference` | blocker | true | An asset file (image, diagram, spreadsheet) is missing or unreadable. The deliverable cannot be rendered or is incomplete. | Re-render or restore the missing asset file before delivery. |
27
+ | `missing_required_section` | blocker | true | A section explicitly required by the brief (e.g., via `--require-section`) is absent. | Add the section the brief requires (matched by --require-section). |
28
+ | `missing_promised_deliverable` | blocker | true | A deliverable (e.g., a slide deck, a chart, a data file) is promised in the brief but not delivered, not marked blocked_capability, and not skipped with a reason. | Produce the promised deliverable or record it as blocked_capability or skipped with a reason. |
29
+
30
+ ### Layout Gate (Rendering & Presentation)
31
+
32
+ | Code | Severity | Integrity | What It Detects | How to Fix |
33
+ |------|----------|-----------|-----------------|-----------|
34
+ | `layout_overflow` | major | false | An element exceeds its container width or height, causing horizontal scroll or content spillage in PDF, DOCX, or slide renders. | Constrain the element to its container (max-width: 100%, overflow-wrap: anywhere, or a narrower measure). |
35
+ | `layout_text_clipped` | major | false | Text is clipped or hidden by `overflow: hidden` or a fixed container height. Content is lost or unreadable. | Remove the overflow: hidden clip or give the text room to wrap. |
36
+ | `layout_scroll_container` | minor | false | A table or list is wrapped in a scrollable container for HTML deliverables. Scrolling content prints clipped in PDF or DOCX. | Scrolling content prints clipped; make the content fit or wrap the table in a scroll wrapper only for HTML deliverables. |
37
+ | `layout_image_distorted` | major | false | An image is distorted, stretched, or squeezed (aspect ratio is not preserved). The visual information is misrepresented. | Preserve the image aspect ratio (object-fit: contain inside a fixed container). |
38
+ | `layout_sibling_overlap` | major | false | Two adjacent block elements overlap (via negative margins, absolute positioning, or grid collision). Layout is broken. | Separate the overlapping elements (margins, grid, or a slide split). |
39
+
40
+ ## Exit Codes
41
+
42
+ Gates emit a manifest-level outcome with one of three statuses:
43
+
44
+ | Status | Meaning |
45
+ |--------|---------|
46
+ | `pass` | All checks passed; no defects raised. |
47
+ | `fail` | One or more defects raised (blocker, major, or minor severity). Repair is required before delivery. |
48
+ | `not_run` | The gate was not invoked (e.g., no probe file provided for layout, no visual inspection conducted). |
49
+
50
+ ### Layout Gate Probe File Requirement
51
+
52
+ The `layout` gate only runs when a probe file (a rendering artifact: PDF, PNG, or HTML snapshot) is provided alongside the manifest. If no probe file is available, the manifest records `layout: not_run` and the gate does not emit defects.
53
+
54
+ ## Lineage and Citation Markers
55
+
56
+ Defect codes and their hints are delivered via the outcome manifest under `residualDefects` (if repair did not exhaust). Each defect entry includes:
57
+
58
+ - **code**: A string key from the table above (e.g., `"em_dash_in_prose"`)
59
+ - **message**: A human-readable narrative or location hint describing where the defect was found (e.g., "Line 42: em dash found in paragraph 2")
60
+
61
+ Citations within reports follow one of two lineage modes:
62
+
63
+ - **inline**: Source marks appear as `[Sn]` or `[Rn]` hyperlinks inline with the cited text.
64
+ - **section**: Sources are listed in a dedicated section with numbered entries ([S1], [S2], etc.).
65
+
66
+ Both modes are valid; choose based on the report's narrative flow.
@@ -0,0 +1,102 @@
1
+ // cli-support.mjs - argument parsing, typed usage errors, exit-code mapping, and JSON file IO
2
+ // shared by every report-tools subcommand. node: builtins only.
3
+
4
+ import { readFileSync, renameSync, writeFileSync } from "node:fs"
5
+ import { basename, dirname, join } from "node:path"
6
+
7
+ /** A usage or input error: always exit code 2. */
8
+ export class CliError extends Error {
9
+ constructor(message) {
10
+ super(message)
11
+ this.name = "CliError"
12
+ }
13
+ }
14
+
15
+ const IO_CODES = new Set(["ENOENT", "EACCES", "EISDIR", "ENOTDIR", "EPERM"])
16
+
17
+ /** 0 pass, 1 semantic failure (result.exitCode), 2 usage or IO error. */
18
+ export function exitCodeFor(resultOrError) {
19
+ if (resultOrError instanceof CliError) return 2
20
+ if (resultOrError instanceof Error) return IO_CODES.has(resultOrError.code) ? 2 : 1
21
+ return resultOrError?.exitCode === 1 ? 1 : 0
22
+ }
23
+
24
+ /**
25
+ * Parse `--name value`, `--name=value`, boolean flags, and repeatable flags.
26
+ * @param {readonly string[]} argv
27
+ * @param {{ repeatable?: string[], booleans?: string[] }} spec
28
+ */
29
+ export function parseArgs(argv, { repeatable = [], booleans = [] } = {}) {
30
+ const repeat = new Set(repeatable)
31
+ const bools = new Set(booleans)
32
+ const positionals = []
33
+ const flags = {}
34
+ for (let index = 0; index < argv.length; index += 1) {
35
+ const arg = argv[index]
36
+ if (!arg.startsWith("--")) {
37
+ positionals.push(arg)
38
+ continue
39
+ }
40
+ const eq = arg.indexOf("=")
41
+ const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq)
42
+ let value
43
+ if (bools.has(name) && eq === -1) value = true
44
+ else if (eq !== -1) value = arg.slice(eq + 1)
45
+ else {
46
+ value = argv[index + 1]
47
+ if (value === undefined || value.startsWith("--")) throw new CliError(`--${name} requires a value`)
48
+ index += 1
49
+ }
50
+ if (repeat.has(name)) flags[name] = [...(flags[name] ?? []), value]
51
+ else flags[name] = value
52
+ }
53
+ return {
54
+ positionals,
55
+ flags,
56
+ /** @param {string} name @param {number} [fallback] */
57
+ int(name, fallback) {
58
+ if (flags[name] === undefined) {
59
+ if (fallback === undefined) throw new CliError(`--${name} is required`)
60
+ return fallback
61
+ }
62
+ const parsed = Number(flags[name])
63
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new CliError(`--${name} requires a positive integer`)
64
+ return parsed
65
+ },
66
+ /** @param {string} name */
67
+ required(name) {
68
+ const value = flags[name]
69
+ if (value === undefined || value === true) throw new CliError(`--${name} is required`)
70
+ return value
71
+ },
72
+ }
73
+ }
74
+
75
+ /** Read and parse a JSON file; a missing or malformed file is a CliError naming the path. */
76
+ export function readJsonFile(path) {
77
+ let text
78
+ try {
79
+ text = readFileSync(path, "utf8")
80
+ } catch (error) {
81
+ throw new CliError(`cannot read ${path}: ${error.code ?? error.message}`)
82
+ }
83
+ try {
84
+ return JSON.parse(text)
85
+ } catch (error) {
86
+ throw new CliError(`${path} is not valid JSON: ${error.message}`)
87
+ }
88
+ }
89
+
90
+ /** Write JSON through a sibling temp file + rename, so a crash never leaves a half-written file. */
91
+ export function writeJsonAtomic(path, value) {
92
+ const temp = join(dirname(path), `.${basename(path)}.${process.pid}.${Date.now()}.tmp`)
93
+ writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`)
94
+ renameSync(temp, path)
95
+ }
96
+
97
+ /** Print a result: JSON on stdout, one summary line on stderr. */
98
+ export function emit(result, { stdout = process.stdout, stderr = process.stderr } = {}) {
99
+ if (typeof result.text === "string") stdout.write(result.text.endsWith("\n") ? result.text : `${result.text}\n`)
100
+ else stdout.write(`${JSON.stringify(result.json ?? result, null, 2)}\n`)
101
+ if (result.summary) stderr.write(`${result.summary}\n`)
102
+ }
@@ -0,0 +1,88 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { mkdtempSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
3
+ import { tmpdir } from "node:os"
4
+ import { join } from "node:path"
5
+ import { pathToFileURL } from "node:url"
6
+
7
+ import { CliError, exitCodeFor, parseArgs, readJsonFile, writeJsonAtomic } from "./cli-support.mjs"
8
+ import { isCliEntry } from "./entry-guard.mjs"
9
+
10
+ function tempDir() {
11
+ return mkdtempSync(join(tmpdir(), "report-tools-"))
12
+ }
13
+
14
+ describe("parseArgs", () => {
15
+ test("#given spaced, equals, repeated, and boolean flags #when parsed #then positionals and flag values are separated", () => {
16
+ // given
17
+ const argv = ["a.html", "--design-spec", "spec.md", "--max-defects=5", "--require-section", "^Methods", "--require-section", "^Sources", "--json", "b"]
18
+ // when
19
+ const parsed = parseArgs(argv, { repeatable: ["require-section"], booleans: ["json"] })
20
+ // then
21
+ expect(parsed.positionals).toEqual(["a.html", "b"])
22
+ expect(parsed.flags["design-spec"]).toBe("spec.md")
23
+ expect(parsed.flags["max-defects"]).toBe("5")
24
+ expect(parsed.flags["require-section"]).toEqual(["^Methods", "^Sources"])
25
+ expect(parsed.flags.json).toBe(true)
26
+ })
27
+
28
+ test("#given a value flag with no value #when parsed #then a CliError names the flag", () => {
29
+ expect(() => parseArgs(["--design-spec"], {})).toThrow(CliError)
30
+ expect(() => parseArgs(["--design-spec"], {})).toThrow("--design-spec")
31
+ })
32
+
33
+ test("#given an integer flag helper #when the value is not a positive integer #then a CliError is thrown", () => {
34
+ const parsed = parseArgs(["--cap", "0"], {})
35
+ expect(() => parsed.int("cap")).toThrow(CliError)
36
+ expect(parseArgs(["--cap", "7"], {}).int("cap")).toBe(7)
37
+ expect(parseArgs([], {}).int("cap", 400)).toBe(400)
38
+ })
39
+ })
40
+
41
+ describe("exitCodeFor", () => {
42
+ test("#given results and errors #when mapped #then pass is 0, semantic failure is 1, usage and IO errors are 2", () => {
43
+ expect(exitCodeFor({ exitCode: 0 })).toBe(0)
44
+ expect(exitCodeFor({ exitCode: 1 })).toBe(1)
45
+ expect(exitCodeFor(new CliError("usage"))).toBe(2)
46
+ const io = Object.assign(new Error("ENOENT"), { code: "ENOENT" })
47
+ expect(exitCodeFor(io)).toBe(2)
48
+ })
49
+ })
50
+
51
+ describe("JSON files", () => {
52
+ test("#given an existing file #when written atomically #then the new content replaces it and no temp file is left", () => {
53
+ // given
54
+ const dir = tempDir()
55
+ const file = join(dir, "state.json")
56
+ writeFileSync(file, '{"old":true}')
57
+ // when
58
+ writeJsonAtomic(file, { fresh: 1 })
59
+ // then
60
+ expect(JSON.parse(readFileSync(file, "utf8"))).toEqual({ fresh: 1 })
61
+ expect(readJsonFile(file)).toEqual({ fresh: 1 })
62
+ const leftovers = readdirSync(dir).filter((name) => name !== "state.json")
63
+ expect(leftovers).toEqual([])
64
+ })
65
+
66
+ test("#given a missing or malformed file #when read #then a CliError names the path", () => {
67
+ const dir = tempDir()
68
+ expect(() => readJsonFile(join(dir, "missing.json"))).toThrow(CliError)
69
+ const bad = join(dir, "bad.json")
70
+ writeFileSync(bad, "{nope")
71
+ expect(() => readJsonFile(bad)).toThrow("bad.json")
72
+ })
73
+ })
74
+
75
+ describe("isCliEntry", () => {
76
+ test("#given argv[1] pointing at this module #when checked #then it is an entry, and another path is not", () => {
77
+ const original = process.argv[1]
78
+ try {
79
+ const self = join(import.meta.dir, "entry-guard.mjs")
80
+ process.argv[1] = self
81
+ expect(isCliEntry(pathToFileURL(self).href)).toBe(true)
82
+ process.argv[1] = join(import.meta.dir, "contracts.mjs")
83
+ expect(isCliEntry(pathToFileURL(self).href)).toBe(false)
84
+ } finally {
85
+ process.argv[1] = original
86
+ }
87
+ })
88
+ })
@@ -0,0 +1,162 @@
1
+ // contracts.mjs - the fixed vocabulary every report-tools module shares.
2
+ //
3
+ // Severities live here and nowhere else: no module exports a way to change them, so a
4
+ // content-integrity defect can never be demoted by a caller. Plain Node ESM, node: builtins
5
+ // only, runnable by `node` and `bun` alike.
6
+
7
+ export const SCHEMA_VERSION = 1
8
+
9
+ function deepFreeze(value) {
10
+ if (value === null || typeof value !== "object") return value
11
+ for (const key of Object.keys(value)) deepFreeze(value[key])
12
+ return Object.freeze(value)
13
+ }
14
+
15
+ /** code -> { severity, integrity, hint }. integrity = blocks delivery even on repair exhaustion. */
16
+ export const DEFECT_CODES = deepFreeze({
17
+ korean_no_keep_all: { severity: "major", integrity: false, hint: 'Add "word-break: keep-all; overflow-wrap: anywhere" to the body or the slide text container.' },
18
+ palette_off_token: { severity: "minor", integrity: false, hint: "Replace the literal color with a design-spec token (var(--...)) or add the color to the spec palette." },
19
+ emoji_in_prose: { severity: "major", integrity: false, hint: "Remove the emoji; use an inline SVG icon or plain text." },
20
+ em_dash_in_prose: { severity: "major", integrity: false, hint: "Rewrite the sentence with a comma, colon, or period instead of an em dash." },
21
+ en_dash_in_prose: { severity: "minor", integrity: false, hint: "Use a hyphen for compounds; keep the en dash only between numbers." },
22
+ heading_too_long: { severity: "minor", integrity: false, hint: "Shorten the heading to one line (26 Korean characters or 12 English words)." },
23
+ heading_generic: { severity: "minor", integrity: false, hint: "Make the heading carry the section's claim instead of a generic label." },
24
+ unsourced_number: { severity: "major", integrity: false, hint: "Tag the number MEASURED / ASSUMED / DERIVED or cite its source in the same paragraph." },
25
+ chart_without_figure: { severity: "major", integrity: false, hint: "Wrap the image or chart in a figure container with a caption." },
26
+ chart_svg_no_text: { severity: "major", integrity: false, hint: "Give the chart a title, axis labels, units, and value labels as SVG text." },
27
+ chart_svg_missing_labels: { severity: "minor", integrity: false, hint: "Add the missing title, axis ticks, or unit label to the chart." },
28
+ korean_serif_font: { severity: "major", integrity: false, hint: "Use a gothic (sans) Korean family; serif and brush families are banned by the design spec." },
29
+ section_without_citation: { severity: "major", integrity: false, hint: "Cite at least one source inside this section or merge it into a cited one." },
30
+ missing_closing_section: { severity: "blocker", integrity: true, hint: "Add the closing section that explains how the report was made (method, sources, gates)." },
31
+ missing_citation_section: { severity: "blocker", integrity: true, hint: "Add a sources or references section, or [S<n>] definition lines." },
32
+ broken_asset_reference: { severity: "blocker", integrity: true, hint: "Re-render or restore the missing asset file before delivery." },
33
+ missing_required_section: { severity: "blocker", integrity: true, hint: "Add the section the brief requires (matched by --require-section)." },
34
+ missing_promised_deliverable: { severity: "blocker", integrity: true, hint: "Produce the promised deliverable or record it as blocked_capability or skipped with a reason." },
35
+ layout_overflow: { severity: "major", integrity: false, hint: "Constrain the element to its container (max-width: 100%, overflow-wrap: anywhere, or a narrower measure)." },
36
+ layout_text_clipped: { severity: "major", integrity: false, hint: "Remove the overflow: hidden clip or give the text room to wrap." },
37
+ layout_scroll_container: { severity: "minor", integrity: false, hint: "Scrolling content prints clipped; make the content fit or wrap the table in a scroll wrapper only for HTML deliverables." },
38
+ layout_image_distorted: { severity: "major", integrity: false, hint: "Preserve the image aspect ratio (object-fit: contain inside a fixed container)." },
39
+ layout_sibling_overlap: { severity: "major", integrity: false, hint: "Separate the overlapping elements (margins, grid, or a slide split)." },
40
+ })
41
+
42
+ const LAYOUT_PREFIX = "layout_"
43
+ /** Codes checkStatic can raise (G1-G15). */
44
+ export const STATIC_CODES = Object.freeze(
45
+ Object.keys(DEFECT_CODES).filter((code) => !code.startsWith(LAYOUT_PREFIX) && code !== "missing_promised_deliverable"),
46
+ )
47
+ /** Codes checkLayout can raise (L1-L5). */
48
+ export const LAYOUT_CODES = Object.freeze(Object.keys(DEFECT_CODES).filter((code) => code.startsWith(LAYOUT_PREFIX)))
49
+ /** Codes only the outcome manifest verifier raises. */
50
+ export const MANIFEST_CODES = Object.freeze(["missing_promised_deliverable"])
51
+
52
+ export const INTEGRITY_CODES = Object.freeze(
53
+ new Set(Object.entries(DEFECT_CODES).filter(([, entry]) => entry.integrity).map(([code]) => code)),
54
+ )
55
+
56
+ export const LANES = Object.freeze(["template-strict", "template-vibe", "no-format", "edit-existing"])
57
+ export const STATES = Object.freeze(["none", "partial", "complete"])
58
+ export const DELIVERABLE_FORMATS = Object.freeze(["pdf", "docx", "html", "md", "latex", "slides", "post"])
59
+ export const DELIVERABLE_STATUSES = Object.freeze(["pending", "delivered", "blocked_capability", "skipped", "failed"])
60
+ export const GATE_NAMES = Object.freeze(["static", "layout", "visual", "proofread"])
61
+ export const GATE_STATUSES = Object.freeze(["pass", "fail", "not_run"])
62
+ export const LINEAGE_MODES = Object.freeze(["inline", "section"])
63
+ export const ANSWERED_BY = Object.freeze(["user", "default", "request"])
64
+ export const REPAIR_DEFAULTS = Object.freeze({ maxAttempts: 3, plateauLimit: 2, wallClockMs: 15 * 60 * 1000 })
65
+ export const DESIGN_SPEC_SECTIONS = Object.freeze([
66
+ "Extracted from",
67
+ "Tokens",
68
+ "Typography",
69
+ "Layout",
70
+ "Structure",
71
+ "Figures",
72
+ "Citations",
73
+ "Open questions",
74
+ ])
75
+
76
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/
77
+
78
+ function isObject(value) {
79
+ return value !== null && typeof value === "object" && !Array.isArray(value)
80
+ }
81
+
82
+ function check(errors, condition, message) {
83
+ if (!condition) errors.push(message)
84
+ }
85
+
86
+ function validateDeliverable(row, index, errors) {
87
+ const at = `deliverables[${index}]`
88
+ if (!isObject(row)) return errors.push(`${at} must be an object`)
89
+ check(errors, DELIVERABLE_FORMATS.includes(row.format), `${at}.format must be one of ${DELIVERABLE_FORMATS.join(", ")}`)
90
+ check(errors, DELIVERABLE_STATUSES.includes(row.status), `${at}.status must be one of ${DELIVERABLE_STATUSES.join(", ")}`)
91
+ check(errors, typeof row.promised === "boolean", `${at}.promised must be a boolean`)
92
+ check(errors, typeof row.updatedAt === "string" && ISO_DATE.test(row.updatedAt), `${at}.updatedAt must be an ISO timestamp`)
93
+ for (const key of ["path", "reason"]) {
94
+ if (row[key] !== undefined) check(errors, typeof row[key] === "string", `${at}.${key} must be a string`)
95
+ }
96
+ for (const key of ["bytes", "pages"]) {
97
+ if (row[key] !== undefined) check(errors, Number.isInteger(row[key]) && row[key] >= 0, `${at}.${key} must be a non-negative integer`)
98
+ }
99
+ }
100
+
101
+ function validateDefect(defect, at, errors) {
102
+ if (!isObject(defect)) return errors.push(`${at} must be an object`)
103
+ check(errors, Object.hasOwn(DEFECT_CODES, defect.code), `${at}.code is not a known defect code`)
104
+ check(errors, typeof defect.message === "string", `${at}.message must be a string`)
105
+ }
106
+
107
+ /** @returns {{ ok: boolean, errors: string[] }} */
108
+ export function validateOutcomeManifest(manifest) {
109
+ const errors = []
110
+ if (!isObject(manifest)) return { ok: false, errors: ["manifest must be an object (schemaVersion missing)"] }
111
+ check(errors, manifest.schemaVersion === SCHEMA_VERSION, `schemaVersion must be ${SCHEMA_VERSION}`)
112
+ check(errors, typeof manifest.sessionDir === "string" && manifest.sessionDir.length > 0, "sessionDir must be a non-empty string")
113
+ check(errors, typeof manifest.startedAt === "string" && ISO_DATE.test(manifest.startedAt), "startedAt must be an ISO timestamp")
114
+ check(errors, manifest.finishedAt === null || (typeof manifest.finishedAt === "string" && ISO_DATE.test(manifest.finishedAt)), "finishedAt must be null or an ISO timestamp")
115
+ check(errors, manifest.elapsedMinutes === null || (Number.isFinite(manifest.elapsedMinutes) && manifest.elapsedMinutes >= 0), "elapsedMinutes must be null or a non-negative number")
116
+ check(errors, manifest.lane === null || LANES.includes(manifest.lane), `lane must be null or one of ${LANES.join(", ")}`)
117
+ check(errors, manifest.interview === null || isObject(manifest.interview), "interview must be null or an object")
118
+ if (Array.isArray(manifest.deliverables)) manifest.deliverables.forEach((row, index) => validateDeliverable(row, index, errors))
119
+ else errors.push("deliverables must be an array")
120
+ if (Array.isArray(manifest.renders)) {
121
+ manifest.renders.forEach((render, index) => {
122
+ check(errors, isObject(render) && typeof render.path === "string" && Number.isInteger(render.page) && render.page >= 1, `renders[${index}] must be { path: string, page: integer >= 1 }`)
123
+ })
124
+ } else errors.push("renders must be an array")
125
+ if (isObject(manifest.gates)) {
126
+ for (const gate of GATE_NAMES) check(errors, GATE_STATUSES.includes(manifest.gates[gate]), `gates.${gate} must be one of ${GATE_STATUSES.join(", ")}`)
127
+ } else errors.push("gates must be an object")
128
+ if (Array.isArray(manifest.residualDefects)) manifest.residualDefects.forEach((defect, index) => validateDefect(defect, `residualDefects[${index}]`, errors))
129
+ else errors.push("residualDefects must be an array")
130
+ check(errors, manifest.repair === null || isObject(manifest.repair), "repair must be null or an object")
131
+ check(errors, manifest.sources === null || isObject(manifest.sources), "sources must be null or an object")
132
+ return { ok: errors.length === 0, errors }
133
+ }
134
+
135
+ function isMeasure(value) {
136
+ return Array.isArray(value) && value.length === 3 && value.every((n) => Number.isInteger(n) && n >= 0)
137
+ }
138
+
139
+ /** @returns {{ ok: boolean, errors: string[] }} */
140
+ export function validateRepairState(state) {
141
+ const errors = []
142
+ if (!isObject(state)) return { ok: false, errors: ["repair state must be an object (schemaVersion missing)"] }
143
+ check(errors, state.schemaVersion === SCHEMA_VERSION, `schemaVersion must be ${SCHEMA_VERSION}`)
144
+ check(errors, typeof state.startedAt === "string" && ISO_DATE.test(state.startedAt), "startedAt must be an ISO timestamp")
145
+ if (isObject(state.budget)) {
146
+ for (const key of ["maxAttempts", "plateauLimit", "wallClockMs"]) {
147
+ check(errors, Number.isInteger(state.budget[key]) && state.budget[key] > 0, `budget.${key} must be a positive integer`)
148
+ }
149
+ } else errors.push("budget must be an object")
150
+ if (Array.isArray(state.attempts)) {
151
+ state.attempts.forEach((attempt, index) => {
152
+ const at = `attempts[${index}]`
153
+ if (!isObject(attempt)) return errors.push(`${at} must be an object`)
154
+ check(errors, typeof attempt.at === "string" && ISO_DATE.test(attempt.at), `${at}.at must be an ISO timestamp`)
155
+ check(errors, isMeasure(attempt.measure), `${at}.measure must be [blocker, major, minor] counts`)
156
+ check(errors, typeof attempt.signature === "string", `${at}.signature must be a string`)
157
+ })
158
+ } else errors.push("attempts must be an array")
159
+ check(errors, Number.isInteger(state.consecutiveNonProgress) && state.consecutiveNonProgress >= 0, "consecutiveNonProgress must be a non-negative integer")
160
+ check(errors, Array.isArray(state.seenSignatures) && state.seenSignatures.every((s) => typeof s === "string"), "seenSignatures must be an array of strings")
161
+ return { ok: errors.length === 0, errors }
162
+ }