@opengsd/gsd-core 1.14.0 → 1.15.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 (283) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ja-JP.md +3 -3
  4. package/README.ko-KR.md +3 -3
  5. package/README.pt-BR.md +3 -3
  6. package/README.zh-CN.md +3 -3
  7. package/agents/gsd-code-fixer.compact.md +7 -6
  8. package/agents/gsd-code-fixer.md +9 -8
  9. package/agents/gsd-debug-session-manager.compact.md +17 -2
  10. package/agents/gsd-debug-session-manager.md +17 -2
  11. package/agents/gsd-debugger.md +2 -2
  12. package/agents/gsd-eval-auditor.compact.md +1 -1
  13. package/agents/gsd-eval-auditor.md +1 -1
  14. package/agents/gsd-executor.md +13 -8
  15. package/agents/gsd-intel-updater.compact.md +1 -1
  16. package/agents/gsd-intel-updater.md +1 -1
  17. package/agents/gsd-phase-researcher.md +19 -11
  18. package/agents/gsd-plan-checker.md +8 -7
  19. package/agents/gsd-planner.md +12 -8
  20. package/agents/gsd-project-researcher.compact.md +1 -1
  21. package/agents/gsd-project-researcher.md +1 -1
  22. package/agents/gsd-research-synthesizer.compact.md +1 -1
  23. package/agents/gsd-research-synthesizer.md +1 -1
  24. package/agents/gsd-ui-auditor.md +155 -17
  25. package/agents/gsd-ui-researcher.compact.md +1 -1
  26. package/agents/gsd-ui-researcher.md +1 -1
  27. package/agents/gsd-verifier.md +10 -9
  28. package/bin/install.js +642 -95
  29. package/commands/gsd/autonomous.md +2 -2
  30. package/commands/gsd/capture.md +1 -1
  31. package/commands/gsd/mempalace-capture.md +7 -3
  32. package/commands/gsd/plan-review-convergence.md +6 -6
  33. package/commands/gsd/progress.md +1 -1
  34. package/commands/gsd/quick-batch.md +1 -1
  35. package/commands/gsd/review.md +2 -3
  36. package/gsd-core/bin/gsd-tools.cjs +335 -22
  37. package/gsd-core/bin/lib/adr-parser.cjs +3 -1
  38. package/gsd-core/bin/lib/audit.cjs +81 -13
  39. package/gsd-core/bin/lib/capability-registry.cjs +82 -187
  40. package/gsd-core/bin/lib/capability-validator.cjs +0 -1
  41. package/gsd-core/bin/lib/check-command-router.cjs +101 -14
  42. package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
  43. package/gsd-core/bin/lib/commands.cjs +175 -42
  44. package/gsd-core/bin/lib/config-loader.cjs +65 -4
  45. package/gsd-core/bin/lib/config.cjs +33 -7
  46. package/gsd-core/bin/lib/decisions.cjs +30 -14
  47. package/gsd-core/bin/lib/frontmatter.cjs +13 -0
  48. package/gsd-core/bin/lib/graphify.cjs +10 -2
  49. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  50. package/gsd-core/bin/lib/init.cjs +207 -41
  51. package/gsd-core/bin/lib/install-engine.cjs +13 -0
  52. package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
  53. package/gsd-core/bin/lib/milestone.cjs +18 -5
  54. package/gsd-core/bin/lib/model-resolver.cjs +159 -50
  55. package/gsd-core/bin/lib/phase-command-router.cjs +9 -1
  56. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  57. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  58. package/gsd-core/bin/lib/phase-id.cjs +109 -7
  59. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  60. package/gsd-core/bin/lib/phase.cjs +227 -26
  61. package/gsd-core/bin/lib/plan-document.cjs +49 -1
  62. package/gsd-core/bin/lib/planning-document.cjs +459 -0
  63. package/gsd-core/bin/lib/planning-inspect.cjs +18 -1
  64. package/gsd-core/bin/lib/planning-workspace.cjs +8 -3
  65. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  66. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  67. package/gsd-core/bin/lib/project-root.cjs +41 -2
  68. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  69. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  70. package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
  71. package/gsd-core/bin/lib/roadmap-parser.cjs +163 -3
  72. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
  73. package/gsd-core/bin/lib/roadmap.cjs +251 -31
  74. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +283 -31
  75. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -1
  76. package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
  77. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -33
  78. package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
  79. package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
  80. package/gsd-core/bin/lib/state-transition.cjs +39 -2
  81. package/gsd-core/bin/lib/state.cjs +42 -0
  82. package/gsd-core/bin/lib/surface.cjs +17 -1
  83. package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
  84. package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
  85. package/gsd-core/bin/lib/uat.cjs +8 -0
  86. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  87. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
  88. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  89. package/gsd-core/bin/lib/verification.cjs +268 -15
  90. package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
  91. package/gsd-core/bin/lib/verify.cjs +132 -25
  92. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  93. package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
  94. package/gsd-core/bin/shared/config-defaults.manifest.json +3 -0
  95. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  96. package/gsd-core/references/checkpoints.md +5 -3
  97. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  98. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  99. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  100. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  101. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  102. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  103. package/gsd-core/references/edge-probe.md +195 -21
  104. package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
  105. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  106. package/gsd-core/references/gsd-run-resolver.md +1 -1
  107. package/gsd-core/references/model-profiles.md +1 -1
  108. package/gsd-core/references/phase-argument-parsing.md +9 -7
  109. package/gsd-core/references/phase-id-convention.md +28 -0
  110. package/gsd-core/references/planner-gap-closure.md +2 -0
  111. package/gsd-core/references/planner-load-graph-context.md +24 -13
  112. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  113. package/gsd-core/references/planning-config.md +11 -2
  114. package/gsd-core/references/tdd.md +27 -4
  115. package/gsd-core/references/ui-consideration-probe.md +10 -5
  116. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  117. package/gsd-core/references/worktree-path-safety.md +321 -0
  118. package/gsd-core/templates/verification-report.md +1 -1
  119. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  120. package/gsd-core/workflows/add-backlog.md +1 -1
  121. package/gsd-core/workflows/add-phase.md +1 -1
  122. package/gsd-core/workflows/add-tests.md +2 -2
  123. package/gsd-core/workflows/add-todo.md +3 -3
  124. package/gsd-core/workflows/ai-integration-phase.md +11 -3
  125. package/gsd-core/workflows/audit-fix.md +1 -1
  126. package/gsd-core/workflows/audit-milestone.md +1 -1
  127. package/gsd-core/workflows/audit-uat.md +1 -1
  128. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  129. package/gsd-core/workflows/autonomous.md +16 -6
  130. package/gsd-core/workflows/check-todos.md +2 -2
  131. package/gsd-core/workflows/cleanup.md +2 -2
  132. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  133. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
  134. package/gsd-core/workflows/code-review-fix.md +108 -22
  135. package/gsd-core/workflows/code-review.md +63 -46
  136. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +1 -1
  137. package/gsd-core/workflows/complete-milestone.md +2 -2
  138. package/gsd-core/workflows/debug.md +3 -3
  139. package/gsd-core/workflows/diagnose-issues.md +1 -1
  140. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  141. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  142. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  143. package/gsd-core/workflows/discuss-phase.md +1 -1
  144. package/gsd-core/workflows/do.md +2 -2
  145. package/gsd-core/workflows/docs-update.md +3 -3
  146. package/gsd-core/workflows/edit-phase.md +1 -1
  147. package/gsd-core/workflows/eval-review.md +10 -3
  148. package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
  149. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
  150. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  151. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +3 -3
  152. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
  153. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  154. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  155. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  156. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  157. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  158. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  159. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  160. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  161. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
  162. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  163. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  164. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
  165. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  166. package/gsd-core/workflows/execute-phase.md +36 -26
  167. package/gsd-core/workflows/execute-plan.md +5 -4
  168. package/gsd-core/workflows/explore.md +3 -3
  169. package/gsd-core/workflows/extract-learnings.md +2 -1
  170. package/gsd-core/workflows/fast.md +1 -1
  171. package/gsd-core/workflows/forensics.md +1 -1
  172. package/gsd-core/workflows/graduation.md +1 -1
  173. package/gsd-core/workflows/health.md +2 -2
  174. package/gsd-core/workflows/help/modes/full.compact.md +3 -3
  175. package/gsd-core/workflows/help/modes/full.md +5 -5
  176. package/gsd-core/workflows/help/modes/topic.md +15 -5
  177. package/gsd-core/workflows/import.md +2 -2
  178. package/gsd-core/workflows/inbox.md +2 -2
  179. package/gsd-core/workflows/ingest-docs.md +3 -3
  180. package/gsd-core/workflows/insert-phase.md +1 -1
  181. package/gsd-core/workflows/list-seeds.md +1 -1
  182. package/gsd-core/workflows/list-workspaces.md +1 -1
  183. package/gsd-core/workflows/manager.md +2 -2
  184. package/gsd-core/workflows/map-codebase.md +2 -2
  185. package/gsd-core/workflows/milestone-summary.md +1 -1
  186. package/gsd-core/workflows/mvp-phase.md +1 -1
  187. package/gsd-core/workflows/new-milestone.md +2 -2
  188. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  189. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  190. package/gsd-core/workflows/new-project.md +7 -7
  191. package/gsd-core/workflows/new-workspace.md +2 -2
  192. package/gsd-core/workflows/next.md +1 -1
  193. package/gsd-core/workflows/note.md +1 -1
  194. package/gsd-core/workflows/onboard.md +1 -1
  195. package/gsd-core/workflows/pause-work.md +1 -1
  196. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  197. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  198. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  199. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  200. package/gsd-core/workflows/plan-phase.md +24 -7
  201. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  202. package/gsd-core/workflows/plant-seed.md +62 -20
  203. package/gsd-core/workflows/pr-branch.md +113 -13
  204. package/gsd-core/workflows/profile-user.md +2 -2
  205. package/gsd-core/workflows/progress.md +1 -1
  206. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
  207. package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
  208. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
  209. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  210. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  211. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  212. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  213. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  214. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  215. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
  216. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  217. package/gsd-core/workflows/quick-batch.md +1 -1
  218. package/gsd-core/workflows/quick.md +21 -9
  219. package/gsd-core/workflows/reapply-patches.md +9 -3
  220. package/gsd-core/workflows/remove-phase.md +1 -1
  221. package/gsd-core/workflows/remove-workspace.md +2 -2
  222. package/gsd-core/workflows/resume-project.md +1 -1
  223. package/gsd-core/workflows/review.md +31 -16
  224. package/gsd-core/workflows/scan.md +1 -1
  225. package/gsd-core/workflows/secure-phase.md +3 -2
  226. package/gsd-core/workflows/settings-advanced.md +30 -10
  227. package/gsd-core/workflows/settings-integrations.md +2 -3
  228. package/gsd-core/workflows/settings.md +4 -4
  229. package/gsd-core/workflows/ship.md +3 -2
  230. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  231. package/gsd-core/workflows/sketch.md +1 -1
  232. package/gsd-core/workflows/smart-entry.md +2 -2
  233. package/gsd-core/workflows/spec-phase.md +15 -5
  234. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  235. package/gsd-core/workflows/spike.md +1 -1
  236. package/gsd-core/workflows/stats.md +1 -1
  237. package/gsd-core/workflows/sync-skills.md +5 -5
  238. package/gsd-core/workflows/thread.md +1 -1
  239. package/gsd-core/workflows/transition.md +1 -1
  240. package/gsd-core/workflows/ui-phase.md +44 -8
  241. package/gsd-core/workflows/ui-review.md +18 -4
  242. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  243. package/gsd-core/workflows/undo.md +339 -20
  244. package/gsd-core/workflows/update.md +7 -7
  245. package/gsd-core/workflows/validate-phase.md +3 -2
  246. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  247. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  248. package/gsd-core/workflows/verify-work.md +81 -16
  249. package/hooks/dist/gsd-agent-isolation-guard.js +24 -0
  250. package/hooks/dist/gsd-secret-read-guard.js +27 -1
  251. package/hooks/dist/gsd-statusline.js +70 -13
  252. package/hooks/dist/gsd-validate-commit.sh +63 -4
  253. package/hooks/gsd-agent-isolation-guard.js +24 -0
  254. package/hooks/gsd-secret-read-guard.js +27 -1
  255. package/hooks/gsd-statusline.js +70 -13
  256. package/hooks/gsd-validate-commit.sh +63 -4
  257. package/package.json +3 -2
  258. package/scripts/build-hooks.js +15 -6
  259. package/scripts/check-contract-drift.cjs +127 -11
  260. package/scripts/command-contract-helpers.cjs +3 -0
  261. package/scripts/docs-guard-registry.cjs +28 -0
  262. package/scripts/gen-loop-host-contract.cjs +69 -0
  263. package/scripts/lib/macos-conformance-tier.generated.cjs +14 -0
  264. package/scripts/lib/ndjson-reporter.cjs +3 -2
  265. package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
  266. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +28 -1
  267. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  268. package/scripts/lint-phase-id-drift.cjs +290 -5
  269. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  270. package/scripts/lint-retired-runtime-name.cjs +619 -0
  271. package/scripts/lint-state-write-path-drift.cjs +93 -0
  272. package/scripts/lint-test-file-count.allowlist.json +28 -9
  273. package/scripts/lint-workflow-shellcheck-baseline.json +15 -0
  274. package/scripts/prompt-injection-scan.sh +4 -0
  275. package/scripts/release-tarball-smoke.cjs +194 -1
  276. package/skills/gsd-autonomous/SKILL.md +2 -2
  277. package/skills/gsd-capture/SKILL.md +1 -1
  278. package/skills/gsd-mempalace-capture/SKILL.md +7 -3
  279. package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
  280. package/skills/gsd-progress/SKILL.md +1 -1
  281. package/skills/gsd-quick-batch/SKILL.md +1 -1
  282. package/skills/gsd-review/SKILL.md +2 -3
  283. package/vscode/package.json +1 -1
@@ -25,6 +25,7 @@ exports.getFieldClassification = getFieldClassification;
25
25
  exports.getPreserveWhenUnchangedFields = getPreserveWhenUnchangedFields;
26
26
  exports.openStateTransaction = openStateTransaction;
27
27
  exports.rebuildStateTransaction = rebuildStateTransaction;
28
+ exports.createStateWriteIntent = createStateWriteIntent;
28
29
  exports.applyPreserveWhenUnchanged = applyPreserveWhenUnchanged;
29
30
  exports.applyStatePreservation = applyStatePreservation;
30
31
  exports.transitionCore = transitionCore;
@@ -401,6 +402,28 @@ function openStateTransaction(init) {
401
402
  function rebuildStateTransaction(init) {
402
403
  return createStateTransaction('rebuild', init, 'rebuildStateTransaction');
403
404
  }
405
+ /**
406
+ * Extend an existing StateTransaction into a StateWriteIntent. The base
407
+ * transaction is REQUIRED — an absent base is a construction failure, mirroring
408
+ * `createStateTransaction`'s ADR-3473 §8.6 posture (do not tolerate null). `scope`
409
+ * defaults to the conservative `'narrow'`; `assertions` defaults to none. Frozen
410
+ * so an intent, like a transaction, cannot be mutated after construction.
411
+ */
412
+ function createStateWriteIntent(transaction, init = {}) {
413
+ if (transaction === null || typeof transaction !== 'object' || Array.isArray(transaction)) {
414
+ const err = new Error('createStateWriteIntent: a base StateTransaction is required (build it with ' +
415
+ 'openStateTransaction / rebuildStateTransaction first). Per ADR-4629 §8.1, an absent ' +
416
+ 'transaction is a construction failure — do not tolerate null.');
417
+ err.code = 'STATE_WRITE_INTENT_TRANSACTION_REQUIRED';
418
+ throw err;
419
+ }
420
+ const assertions = Object.freeze((init.assertions ?? []).map((a) => Object.freeze({ field: a.field, requirement: a.requirement })));
421
+ return Object.freeze({
422
+ ...transaction,
423
+ assertions,
424
+ scope: init.scope ?? 'narrow',
425
+ });
426
+ }
404
427
  /**
405
428
  * ADR-3408 §8.2: an unenforced `preserve-when-unchanged` row throws. Both
406
429
  * ends of this invariant are gsd-core's own source — a declared row the
@@ -1648,8 +1671,22 @@ function completePhaseCore(content, intent, deps) {
1648
1671
  body = statusAfter;
1649
1672
  updated.push('Status');
1650
1673
  }
1651
- // Current Plan — reset for the next phase.
1652
- const planAfter = (0, state_document_cjs_1.stateReplaceFieldWithFallback)(body, 'Current Plan', 'Plan', 'Not started');
1674
+ // Current Plan — reset for the next phase. #4823: when a Current Position
1675
+ // section exists (the canonical layout, #2956 locator), the reset is scoped
1676
+ // to it — the whole-body fallback 'Plan' run matched any hard-wrapped prose
1677
+ // line starting with `plan:` anywhere in the document and rewrote it to
1678
+ // 'Not started', silently destroying narrative. Legacy sectionless layouts
1679
+ // (fields at top level, no section) keep the whole-body behavior unchanged:
1680
+ // there is no section to scope to, and their fields are the intended
1681
+ // targets.
1682
+ let planAfter;
1683
+ const positionScope = (0, state_document_cjs_1.stateCurrentPositionSlice)(body);
1684
+ if (positionScope !== null) {
1685
+ planAfter = (0, markdown_sectionizer_cjs_1.withSection)(body, (h) => (h.level === 2 || h.level === 3) && h.text.trim().toLowerCase() === 'current position', (sectionBody) => (0, state_document_cjs_1.stateReplaceFieldWithFallback)(sectionBody, 'Current Plan', 'Plan', 'Not started'), { levelBounded: true });
1686
+ }
1687
+ else {
1688
+ planAfter = (0, state_document_cjs_1.stateReplaceFieldWithFallback)(body, 'Current Plan', 'Plan', 'Not started');
1689
+ }
1653
1690
  if (planAfter !== body) {
1654
1691
  body = planAfter;
1655
1692
  updated.push('Current Plan');
@@ -1609,10 +1609,31 @@ function cmdStateRecordSession(cwd, options, raw) {
1609
1609
  const updated = [];
1610
1610
  let sessionCreated = false;
1611
1611
  const divergedFields = [];
1612
+ // #4763 (1): last-writer-wins stays (the recorded single-slot handoff design),
1613
+ // but a displaced record is no longer silent. The pre-write session record is
1614
+ // captured below and surfaced in the payload under `replacedRecord` whenever a
1615
+ // replacement actually changes a non-empty prior value.
1616
+ const priorRecord = {};
1612
1617
  // ADR-3473 §8.7 (#3872): caller-allocated out-param, filled with the
1613
1618
  // transaction's own pre-write snapshot + body by `applyPostSyncPreservation`.
1614
1619
  const preWriteState = {};
1615
1620
  readModifyWriteStateMd(statePath, (content) => {
1621
+ // #4763 (1): read the pre-write session record. The capture mirrors the
1622
+ // WRITER, not the snapshot reader: stateReplaceField replaces the FIRST
1623
+ // case-insensitive label match anywhere in the document, so the capture is
1624
+ // document-wide too — scoping it to ## Session would miss an archive-section
1625
+ // line the writer actually displaced. Continuation lines join via
1626
+ // stateFieldContinuation (the sanctioned joiner — stateExtractField alone is
1627
+ // first-line-only), so a wrapped multi-line handoff is surfaced whole.
1628
+ const capturePrior = (fieldName) => {
1629
+ const first = (0, state_document_cjs_1.stateExtractField)(content, fieldName);
1630
+ if (first === null)
1631
+ return undefined;
1632
+ const cont = (0, state_document_cjs_1.stateFieldContinuation)(content, fieldName);
1633
+ return cont ? `${first}\n${cont}` : first;
1634
+ };
1635
+ priorRecord.stoppedAt = capturePrior('Stopped At');
1636
+ priorRecord.resumeFile = capturePrior('Resume File');
1616
1637
  // Update Last session / Last Date
1617
1638
  let result = (0, state_document_cjs_1.stateReplaceField)(content, 'Last session', now);
1618
1639
  if (result) {
@@ -1836,6 +1857,27 @@ function cmdStateRecordSession(cwd, options, raw) {
1836
1857
  const result = { recorded: true, updated: reconciledUpdated };
1837
1858
  if (sessionCreated)
1838
1859
  result['created'] = true;
1860
+ // #4763 (1): surface any non-empty prior record the write displaced. Gated
1861
+ // on reconciledUpdated (the fields that actually persisted, post-#3957
1862
+ // reconcile) rather than the pre-reconciliation updated[]. Only fields with
1863
+ // real prior content differing from the caller's value count — same-value
1864
+ // rewrites, the insert path (no prior label), and the #944 template-default
1865
+ // DWIM (defaults match case-insensitively, so a case-variant 'none' →
1866
+ // 'None' rewrite is normalization, not displacement) are excluded.
1867
+ const isResumeTemplateDefault = priorRecord.resumeFile !== undefined
1868
+ && state_document_cjs_1.KNOWN_TEMPLATE_DEFAULTS['Resume File'].some((d) => d.toLowerCase() === priorRecord.resumeFile?.toLowerCase());
1869
+ const replacedRecord = {};
1870
+ if (reconciledUpdated.includes('Stopped At') && priorRecord.stoppedAt
1871
+ && priorRecord.stoppedAt !== options.stopped_at) {
1872
+ replacedRecord['Stopped At'] = priorRecord.stoppedAt;
1873
+ }
1874
+ if (reconciledUpdated.includes('Resume File') && priorRecord.resumeFile
1875
+ && !isResumeTemplateDefault
1876
+ && priorRecord.resumeFile !== (options.resume_file ?? undefined)) {
1877
+ replacedRecord['Resume File'] = priorRecord.resumeFile;
1878
+ }
1879
+ if (Object.keys(replacedRecord).length > 0)
1880
+ result['replacedRecord'] = replacedRecord;
1839
1881
  output(result, raw, 'true');
1840
1882
  }
1841
1883
  else if (updated.length === 0) {
@@ -61,6 +61,18 @@ const runtimeArtifactInstallPlan = require("./runtime-artifact-install-plan.cjs"
61
61
  const retiredArtifactCleanup = require("./retired-artifact-cleanup.cjs");
62
62
  const { assertDestWithinConfigHome } = runtimeArtifactInstallPlan;
63
63
  const SURFACE_FILE_NAME = '.gsd-surface.json';
64
+ const INSTALL_MANIFEST_FILE_NAME = 'gsd-file-manifest.json';
65
+ /** Read only the installer-owned relative-include style, failing closed. */
66
+ function readRelativeIncludePrefix(runtimeConfigDir) {
67
+ try {
68
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(runtimeConfigDir, INSTALL_MANIFEST_FILE_NAME), 'utf8'));
69
+ const prefix = parsed?.['relativeIncludePrefix'];
70
+ return typeof prefix === 'string' ? prefix : undefined;
71
+ }
72
+ catch {
73
+ return undefined;
74
+ }
75
+ }
64
76
  /**
65
77
  * Read the surface state from a runtime config directory.
66
78
  *
@@ -341,7 +353,11 @@ function applySurface(runtimeConfigDir, layout, manifest, clusterMap, registry,
341
353
  const _isGlobal = (0, install_scope_cjs_1.isGlobalScope)(layout.scope ?? 'global');
342
354
  const _isOpencode = layout.runtime === 'opencode';
343
355
  const _isWindowsHost = (opts?.platform ?? process.platform) === 'win32';
344
- const _pathPrefix = runtimeArtifactConversion._computePathPrefix({ isGlobal: _isGlobal, isOpencode: _isOpencode, isWindowsHost: _isWindowsHost, resolvedTarget: _resolvedTarget, homeDir: _homeDir });
356
+ // #4377: style is an install-time fact, not a process environment setting.
357
+ // A later gsd-tools surface apply runs in another process, so it must reuse
358
+ // the persisted, root-relative prefix the installer actually emitted.
359
+ const _relativeIncludePrefix = readRelativeIncludePrefix(runtimeConfigDir);
360
+ const _pathPrefix = runtimeArtifactConversion._computePathPrefix({ isGlobal: _isGlobal, isOpencode: _isOpencode, isWindowsHost: _isWindowsHost, resolvedTarget: _resolvedTarget, homeDir: _homeDir, projectRelative: !_isGlobal && typeof _relativeIncludePrefix === 'string', projectRelativePath: _relativeIncludePrefix, localDirName: runtimeArtifactConversion._localIncludeDirName(layout.runtime) });
345
361
  const _attribution = opts?.resolveAttribution ? opts.resolveAttribution(layout.runtime) : undefined;
346
362
  // #2875 Part 2 (row I1): layout.configDir is this call's install root.
347
363
  const agentCtx = { runtime: layout.runtime, pathPrefix: _pathPrefix, attribution: _attribution, targetDir: layout.configDir };
@@ -34,6 +34,52 @@ const prohibition_enforcement_cjs_1 = require("./prohibition-enforcement.cjs");
34
34
  function baseOf(p) {
35
35
  return typeof p === 'string' ? (p.split(/[\\/]/).pop() ?? p) : '';
36
36
  }
37
+ /**
38
+ * #4724 — Surefire/Failsafe XML summary parsed by TAG-BOUNDARY scanning, NOT
39
+ * by a lazy `(.*?)<\/testcase>` regex: Surefire writes PASSING cases as
40
+ * self-closing `<testcase … />` elements, so a lazy span from a green case to
41
+ * the next closing tag swallows every case between and misreports names beside
42
+ * the right failure message. The scanner walks `<testcase` start tags; each
43
+ * case's extent is its own tag (self-closing) or the segment up to its
44
+ * `</testcase>` closer (testcases never nest). A `<failure` or `<error` child
45
+ * marks the case failing; the reported name is `classname#name`. Any parse
46
+ * anomaly degrades to "case not failing" — the module stays fail-closed.
47
+ */
48
+ function parseSurefireSummary(output) {
49
+ let tests = 0;
50
+ let fail = 0;
51
+ const failing_tests = [];
52
+ let idx = output.indexOf('<testcase');
53
+ while (idx !== -1) {
54
+ const tagEnd = output.indexOf('>', idx);
55
+ // #4724 review (HIGH): a `<testcase` with no `>` (truncated/stream-cut
56
+ // output) must terminate the scan — resuming the search at -1 clamps to 0
57
+ // and re-finds the same tag forever. Degrade to what was scanned so far
58
+ // (fail-closed: an incomplete report proves nothing).
59
+ if (tagEnd === -1)
60
+ break;
61
+ tests++;
62
+ const tagText = output.slice(idx, tagEnd + 1);
63
+ // Self-closing (`/>`): the case has NO body — a failing sibling after it
64
+ // must never be attributed to this case (the #4724 spanning trap).
65
+ const selfClosing = output[tagEnd - 1] === '/';
66
+ const closeIdx = selfClosing ? -1 : output.indexOf('</testcase>', tagEnd);
67
+ // CDATA sections are verbatim captured output (e.g. System.out echoing
68
+ // test source) — their content must never be scanned for failure tags
69
+ // (#4724 review: CDATA phantom failures).
70
+ const body = (selfClosing
71
+ ? ''
72
+ : output.slice(tagEnd + 1, closeIdx === -1 ? output.length : closeIdx)).replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, '');
73
+ const name = /name="([^"]*)"/.exec(tagText)?.[1] ?? '';
74
+ const classname = /classname="([^"]*)"/.exec(tagText)?.[1] ?? '';
75
+ if (body.includes('<failure') || body.includes('<error')) {
76
+ fail++;
77
+ failing_tests.push(classname ? `${classname}#${name}` : name);
78
+ }
79
+ idx = output.indexOf('<testcase', closeIdx === -1 ? tagEnd : closeIdx);
80
+ }
81
+ return { tests, pass: tests - fail, fail, failing_tests };
82
+ }
37
83
  /** Coerce and validate the raw record's scalar fields. Returns null exit_code only when absent/non-numeric. */
38
84
  function readInput(input) {
39
85
  const command = typeof input?.command === 'string' ? input.command : '';
@@ -69,8 +115,34 @@ function classifyRedEvidence(input) {
69
115
  };
70
116
  }
71
117
  const { command, exitCode, output, targetTest } = parsed;
72
- const summary = (0, prohibition_enforcement_cjs_1.parseNodeTestSummary)(output);
73
- const failing = (0, prohibition_enforcement_cjs_1.tapFailedTestNames)(output);
118
+ // #4724: format detection. Surefire/Failsafe XML (a `<testsuite>` element) is
119
+ // parsed by tag-boundary scanning; everything else stays on the proven TAP
120
+ // primitives. Unknown formats keep the TAP parse — which finds nothing — and
121
+ // fail closed below.
122
+ // #4724 review: BOTH element names must appear — a TAP error message that
123
+ // merely quotes "<testsuite>" (e.g. "expected <testsuite> was 2") must not
124
+ // flip a genuine TAP red into the XML path.
125
+ const isSurefireXml = output.includes('<testsuite') && output.includes('<testcase');
126
+ let summary;
127
+ let failing;
128
+ if (isSurefireXml) {
129
+ const sf = parseSurefireSummary(output);
130
+ summary = { tests: sf.tests, pass: sf.pass, fail: sf.fail };
131
+ failing = sf.failing_tests;
132
+ }
133
+ else {
134
+ summary = (0, prohibition_enforcement_cjs_1.parseNodeTestSummary)(output);
135
+ failing = (0, prohibition_enforcement_cjs_1.tapFailedTestNames)(output);
136
+ }
137
+ // Surefire target matching is class-level (the plan names the class); the
138
+ // entry's last classname segment or its test name must carry the target.
139
+ const surefireTargetMatches = isSurefireXml
140
+ ? failing.some((entry) => {
141
+ const cls = entry.split('#')[0] ?? '';
142
+ const nm = entry.split('#')[1] ?? '';
143
+ return cls === targetTest || cls.endsWith(`.${targetTest}`) || nm === targetTest;
144
+ })
145
+ : failing.includes(targetTest);
74
146
  const evidence = {
75
147
  command,
76
148
  exit_code: exitCode,
@@ -90,14 +162,15 @@ function classifyRedEvidence(input) {
90
162
  if (summary.tests === 0) {
91
163
  return { verdict: 'INVALID_RED', reason: 'zero_tests_discovered', evidence };
92
164
  }
93
- // Nonzero exit but TAP reports no failing test: harness/setup/parser crash
94
- // whose failure never reached a test assertion (or unparseable output).
165
+ // Nonzero exit but the report shows no failing test: harness/setup/parser
166
+ // crash whose failure never reached a test assertion (or unparseable output).
95
167
  if (summary.fail === 0 || failing.length === 0) {
96
168
  return { verdict: 'INVALID_RED', reason: 'nonzero_exit_without_test_failure', evidence };
97
169
  }
98
170
  // Fixture/load failure: every failing entry is named like the target FILE —
99
171
  // node reports a load-time crash (throw-on-require, syntax error, ENOENT
100
172
  // fixture) as a file-named `not ok 1 - <file>`, never the target test.
173
+ // (TAP-only shape: Surefire failures carry class#method names.)
101
174
  const targetBase = baseOf(input?.targetFile ?? '');
102
175
  const distinctlyNamed = failing.filter((n) => (targetBase ? baseOf(n) !== targetBase : true));
103
176
  if (distinctlyNamed.length === 0) {
@@ -105,7 +178,7 @@ function classifyRedEvidence(input) {
105
178
  }
106
179
  // Unrelated failure: real tests ran and failed, but none is the target test
107
180
  // the plan named — an unrelated assertion must not authorize GREEN.
108
- if (!distinctlyNamed.includes(targetTest)) {
181
+ if (!(isSurefireXml ? surefireTargetMatches : distinctlyNamed.includes(targetTest))) {
109
182
  return { verdict: 'INVALID_RED', reason: 'no_target_test_failure', evidence };
110
183
  }
111
184
  return { verdict: 'RED_EVIDENCE_OK', reason: 'target_test_failed', evidence };
@@ -47,6 +47,22 @@ const BLOCKING_VERIFICATION_FM_STATUSES = new Set([
47
47
  ]);
48
48
  // UAT test-item `result` values that count as passing
49
49
  const PASSING_RESULTS = new Set(['passed', 'pass']);
50
+ // #4546 — a `skipped` test-item whose reason carries the verify-work writer's
51
+ // deferral template prefix ("Deferred follow-up: …", #1921) is a deliberately
52
+ // deferred follow-up: non-blocking. Quote-tolerant (the writer wraps the value
53
+ // in double quotes) and case-insensitive (human-edited files vary). Anything
54
+ // else — a reasonless skip, a non-deferral reason — still blocks. PARITY: this
55
+ // matcher and the writer template in gsd-core/workflows/verify-work.md
56
+ // (process_response) are two halves of one contract, pinned together by
57
+ // tests/verify-work-deferred-promotion.test.cjs.
58
+ const DEFERRED_REASON_RE = /^["']?deferred follow-up\b/i;
59
+ // Trust note (#4546 review): the reason line is user-authored state — an
60
+ // author could equally write `result: passed` — so this prefix is an
61
+ // AUTHORING contract with the verify-work writer, not a security boundary.
62
+ // A hand-written deferral that skips the UAT file's ## Deferred Follow-Ups
63
+ // section also bypasses the complete_session promotion offer; the section is
64
+ // the durable project-level record. Variant spellings that do not match
65
+ // ("Deferred follow-ups:", "followup") block — fail-closed by design.
50
66
  // ─── stripFalsePositiveContexts ───────────────────────────────────────────────
51
67
  /**
52
68
  * Remove contexts that can contain `result: ...` lines that are NOT real test results:
@@ -166,16 +182,28 @@ function parseUatResultItems(cleanContent) {
166
182
  // ambiguity counting, matching src/uat.cts's contract.
167
183
  // Uses [ \t]* (not \s*) so the captured value must sit on the SAME line as result:.
168
184
  // A result: key with the value on a subsequent line yields no match → 'missing' (blocker).
185
+ // #4546: the `reason:` line is captured with the same frame and FIRST-match
186
+ // rule — it is the deferral signal the evaluator needs (a `skipped` item
187
+ // whose reason is the verify-work writer's "Deferred follow-up:" template
188
+ // is non-blocking). Quoted values are captured with their quotes so the
189
+ // evaluator's matcher can tolerate them exactly as written.
169
190
  const RESULT_LINE_RE = /^result:[ \t]*\[?([\w-]+)\]?/i;
170
191
  const resultMatch = blockContent
171
192
  .split('\n')
172
193
  .map((line) => line.match(RESULT_LINE_RE))
173
194
  .find((m) => m !== null) ?? null;
195
+ const REASON_LINE_RE = /^reason:[ \t]*(.*)$/i;
196
+ const reasonMatch = blockContent
197
+ .split('\n')
198
+ .map((line) => line.match(REASON_LINE_RE))
199
+ .find((m) => m !== null) ?? null;
200
+ const reason = reasonMatch ? reasonMatch[1].trim() : '';
174
201
  if (resultMatch) {
175
202
  items.push({
176
203
  test: h.test,
177
204
  name: h.name,
178
205
  result: resultMatch[1].toLowerCase(),
206
+ reason,
179
207
  });
180
208
  }
181
209
  else {
@@ -184,6 +212,7 @@ function parseUatResultItems(cleanContent) {
184
212
  test: h.test,
185
213
  name: h.name,
186
214
  result: 'missing',
215
+ reason,
187
216
  });
188
217
  }
189
218
  }
@@ -198,7 +227,13 @@ function parseUatResultItems(cleanContent) {
198
227
  * Pass requires at least one real passing check AND no blockers.
199
228
  */
200
229
  function evaluateUatPassed(phaseFullDir, opts) {
201
- const requireVerification = opts?.policy?.requireVerification === true;
230
+ // uatOnly (#4663) takes precedence: it evaluates the UAT rows ONLY, skipping
231
+ // the VERIFICATION-file blockers entirely. The verify-work canonicalize
232
+ // pre-check needs exactly that — it runs while the report still reads
233
+ // `human_needed`, which is itself a blocking verification status, so the
234
+ // full predicate could never pass there and the flip would deadlock.
235
+ const uatOnly = opts?.policy?.uatOnly === true;
236
+ const requireVerification = !uatOnly && opts?.policy?.requireVerification === true;
202
237
  const blockers = [];
203
238
  const checks = [];
204
239
  const uatFiles = [];
@@ -221,7 +256,7 @@ function evaluateUatPassed(phaseFullDir, opts) {
221
256
  checks: [],
222
257
  blockers,
223
258
  no_uat_artifacts,
224
- policy: { require_verification: requireVerification },
259
+ policy: { require_verification: requireVerification, uat_only: uatOnly },
225
260
  // readVerificationStatus was never reached on this early-return path.
226
261
  verification_stale_check_indeterminate: false,
227
262
  };
@@ -269,13 +304,20 @@ function evaluateUatPassed(phaseFullDir, opts) {
269
304
  const cleanContent = stripFalsePositiveContexts(raw);
270
305
  const items = parseUatResultItems(cleanContent);
271
306
  for (const item of items) {
272
- const passing = PASSING_RESULTS.has(item.result);
307
+ // #4546: a `skipped` item whose reason matches the verify-work writer's
308
+ // "Deferred follow-up:" template is a deliberately deferred follow-up
309
+ // (#1921) — non-blocking. Quote-tolerant because the writer emits the
310
+ // reason WITH its wrapping quotes. Everything else — pending, blocked,
311
+ // issue, missing, and a plain or non-deferral skipped — still blocks.
312
+ const deferred = item.result === 'skipped' && DEFERRED_REASON_RE.test(item.reason);
313
+ const passing = PASSING_RESULTS.has(item.result) || deferred;
273
314
  checks.push({
274
315
  file,
275
316
  test: item.test,
276
317
  name: item.name,
277
318
  result: item.result,
278
319
  passing,
320
+ deferred,
279
321
  });
280
322
  if (!passing) {
281
323
  blockers.push(`${file}: test ${item.test} (${item.result})`);
@@ -284,7 +326,7 @@ function evaluateUatPassed(phaseFullDir, opts) {
284
326
  }
285
327
  // ── Process VERIFICATION files ─────────────────────────────────────────────
286
328
  let hasPassingVerification = false;
287
- for (const file of verFileNames) {
329
+ for (const file of uatOnly ? [] : verFileNames) {
288
330
  verificationFiles.push(file);
289
331
  const verificationFilePath = node_path_1.default.join(phaseFullDir, file);
290
332
  let raw = '';
@@ -340,6 +382,7 @@ function evaluateUatPassed(phaseFullDir, opts) {
340
382
  no_uat_artifacts,
341
383
  policy: {
342
384
  require_verification: requireVerification,
385
+ uat_only: uatOnly,
343
386
  },
344
387
  verification_stale_check_indeterminate: verificationStaleCheckIndeterminate,
345
388
  };
@@ -3579,6 +3579,14 @@ function categorizeItem(rawResult, reason, blockedBy) {
3579
3579
  return 'blocked';
3580
3580
  }
3581
3581
  if (result === 'skipped') {
3582
+ // #4546 — a deliberately deferred follow-up (the verify-work writer's
3583
+ // "Deferred follow-up:" template, #1921) is its own category, checked
3584
+ // BEFORE the keyword families so e.g. "…on the release build next
3585
+ // version" is not misfiled as build_needed. Must agree with
3586
+ // uat-predicate.cts's DEFERRED_REASON_RE (gate/audit agreement,
3587
+ // #3078-CR), pinned by tests/uat-predicate.test.cjs.
3588
+ if (reason && /^["']?deferred follow-up\b/i.test(reason))
3589
+ return 'deferred';
3582
3590
  if (reason) {
3583
3591
  if (/server|not running|not available/i.test(reason))
3584
3592
  return 'server_blocked';
@@ -117,6 +117,15 @@ function validateRequirement(element) {
117
117
  if (r.elements == null && !(typeof r.text === 'string' && r.text.trim())) {
118
118
  throw new Error(`element ${element.id} text must be a non-empty string when no elements override is provided`);
119
119
  }
120
+ // text_en (#4657) is optional, but when present it must be a non-empty string. An empty
121
+ // string is NOT caught by `??` (only null/undefined are), so an unvalidated `text_en: ''`
122
+ // would silently win `text_en ?? text` and classify against '' — the same fail-open shape
123
+ // #1110/#2773 already exist to eliminate, just moved one field over. Validated
124
+ // unconditionally (not gated on whether `elements` will make it unused) so bad data fails
125
+ // closed even when it happens to be dead for this particular call.
126
+ if (r.text_en != null && !(typeof r.text_en === 'string' && r.text_en.trim())) {
127
+ throw new Error(`element ${element.id} text_en must be a non-empty string when present`);
128
+ }
120
129
  }
121
130
  /** Validate a UI-consideration resolution against the UI verification vocabulary (delegated, D-06). */
122
131
  function validateResolution(resolution) {
@@ -143,7 +152,11 @@ function proposeConsiderations(element) {
143
152
  kinds = element.elements;
144
153
  }
145
154
  else {
146
- kinds = classifyElement(element.text);
155
+ // #4657: prefer the English translation when present — UI_CUES are English-only
156
+ // word-boundary patterns, so a non-English `text` (e.g. response_language projects)
157
+ // would otherwise classify to zero kinds. validateRequirement (called above) has
158
+ // already guaranteed text_en, if present, is a non-empty string.
159
+ kinds = classifyElement((element.text_en ?? element.text));
147
160
  if (kinds.length === 0) {
148
161
  // Prose present but no element cue matched. Do NOT silently drop it (#1110): a UI element
149
162
  // whose phrasing missed every cue would otherwise vanish from coverage with no signal — the
@@ -210,7 +223,7 @@ function proposeElements(elements) {
210
223
  const considerations = proposeConsiderations(el);
211
224
  const kinds = Array.isArray(el.elements)
212
225
  ? el.elements // already validated inside proposeConsiderations
213
- : classifyElement(el.text);
226
+ : classifyElement((el.text_en ?? el.text)); // #4657: text_en ?? text
214
227
  const unclassified = !Array.isArray(el.elements) && kinds.length === 0;
215
228
  const categories = unclassified ? [] : applicableCategories(kinds);
216
229
  return { id: el.id, kinds, categories, considerations, unclassified };
@@ -23,6 +23,19 @@
23
23
  * html, ...) are deliberately NOT static evidence: docs sites and
24
24
  * markdown/bash/config repos routinely carry stray `.html`/`.css`, which
25
25
  * is precisely the false-positive class #3312 reports.
26
+ * (c) any `*.xaml` file, or a `*.swift` / `*.kt` / `*.dart` file whose
27
+ * content carries its ecosystem's UI-framework import marker
28
+ * (`import SwiftUI` / `import UIKit`, `androidx.compose`,
29
+ * `package:flutter`) (#4658). Native UI projects (SwiftUI, Jetpack
30
+ * Compose, Flutter, .NET MAUI) carry neither (a) nor (b), which made the
31
+ * gate structurally unreachable for them. Source files match on the
32
+ * IMPORT, not the extension alone — the same unambiguity bar that
33
+ * justifies (b)'s subset and excludes (css, scss, html): a non-UI Swift
34
+ * package (a CLI, a server) imports Foundation, not SwiftUI, and must
35
+ * stay silent. `.xaml` is extension-alone for the same reason `.tsx` is —
36
+ * the extension itself is unambiguous. Marker matching is case-sensitive
37
+ * (imports are case-sensitive in all four ecosystems); extension
38
+ * matching is case-insensitive, mirroring `UI_COMPONENT_FILE_RE`.
26
39
  *
27
40
  * All I/O failures degrade to `false` (no evidence) — never throw.
28
41
  */
@@ -30,12 +43,28 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
30
43
  return (mod && mod.__esModule) ? mod : { "default": mod };
31
44
  };
32
45
  Object.defineProperty(exports, "__esModule", { value: true });
33
- exports.UI_COMPONENT_FILE_RE = void 0;
46
+ exports.NATIVE_UI_CONTENT_MARKERS = exports.NATIVE_UI_XAML_RE = exports.NATIVE_UI_SOURCE_RE = exports.UI_COMPONENT_FILE_RE = void 0;
34
47
  exports.hasStaticFrontendEvidence = hasStaticFrontendEvidence;
35
48
  const node_fs_1 = __importDefault(require("node:fs"));
36
49
  const node_path_1 = __importDefault(require("node:path"));
37
50
  /** Component-framework file extensions — the static-evidence subset of UI_FILE_EXTENSIONS_RE. */
38
51
  exports.UI_COMPONENT_FILE_RE = /\.(tsx|jsx|vue|svelte)$/i;
52
+ /** Native UI source files whose CONTENT is scanned for an import marker (#4658). */
53
+ exports.NATIVE_UI_SOURCE_RE = /\.(swift|kt|dart)$/i;
54
+ /** Native UI files that are evidence by extension alone — `.xaml`, the `.tsx` analogue. */
55
+ exports.NATIVE_UI_XAML_RE = /\.xaml$/i;
56
+ /**
57
+ * Per-extension UI-framework import markers for `NATIVE_UI_SOURCE_RE` files
58
+ * (#4658). Every marker embeds its ecosystem's import keyword, so a bare
59
+ * framework-name mention in prose or a comment is not evidence; the Dart
60
+ * marker carries both legal quote styles. Case-sensitive: imports are
61
+ * case-sensitive in Swift, Kotlin and Dart.
62
+ */
63
+ exports.NATIVE_UI_CONTENT_MARKERS = {
64
+ '.swift': ['import SwiftUI', 'import UIKit'],
65
+ '.kt': ['import androidx.compose'],
66
+ '.dart': ["import 'package:flutter", 'import "package:flutter'],
67
+ };
39
68
  /**
40
69
  * UI-framework package.json dependencies (dependencies OR devDependencies).
41
70
  * Component frameworks/renderers only — deliberately excludes meta tooling
@@ -111,9 +140,16 @@ function packageJsonHasUiFramework(projectDir) {
111
140
  }
112
141
  return false;
113
142
  }
114
- function treeHasComponentFile(projectDir) {
115
- // Iterative BFS — bounded by MAX_WALK_ENTRIES so a pathological tree cannot
116
- // stall the gate. Symlinks are never followed (withFileTypes + isDirectory).
143
+ /**
144
+ * Shared bounded BFS over the project tree — the single home of the walk
145
+ * semantics both evidence walks depend on: SKIP_DIRS pruning, the
146
+ * MAX_WALK_ENTRIES entry cap (cap-hit → `false`: the tree is treated as
147
+ * scanned and evidence stays undecided), symlinks never followed
148
+ * (withFileTypes Dirents), unreadable directories skipped. `visit` is called
149
+ * for every regular file with its name and full path; returning `true` stops
150
+ * the walk with `true` (evidence found).
151
+ */
152
+ function walkProjectFiles(projectDir, visit) {
117
153
  const queue = [projectDir];
118
154
  let visited = 0;
119
155
  while (queue.length > 0 && visited < MAX_WALK_ENTRIES) {
@@ -133,25 +169,80 @@ function treeHasComponentFile(projectDir) {
133
169
  if (!SKIP_DIRS.has(entry.name))
134
170
  queue.push(node_path_1.default.join(dir, entry.name));
135
171
  }
136
- else if (entry.isFile() && exports.UI_COMPONENT_FILE_RE.test(entry.name)) {
172
+ else if (entry.isFile() && visit(entry.name, node_path_1.default.join(dir, entry.name))) {
137
173
  return true;
138
174
  }
139
175
  }
140
176
  }
141
177
  return false;
142
178
  }
179
+ function treeHasComponentFile(projectDir) {
180
+ return walkProjectFiles(projectDir, (name) => exports.UI_COMPONENT_FILE_RE.test(name));
181
+ }
182
+ /**
183
+ * Read at most the first 64 KiB of `file` and report whether any of `markers`
184
+ * occurs in it (#4658). Import sections live at the top of a source file, so a
185
+ * bounded prefix read keeps the gate's plan-time cost profile without reading
186
+ * generated monsters in full. Any I/O failure degrades to false — never throw.
187
+ */
188
+ function fileHasAnyMarker(file, markers) {
189
+ let fd;
190
+ try {
191
+ fd = node_fs_1.default.openSync(file, 'r');
192
+ }
193
+ catch {
194
+ return false;
195
+ }
196
+ try {
197
+ const bytes = Buffer.alloc(64 * 1024);
198
+ const read = node_fs_1.default.readSync(fd, bytes, 0, bytes.length, 0);
199
+ const prefix = bytes.toString('utf8', 0, read);
200
+ return markers.some((m) => prefix.includes(m));
201
+ }
202
+ catch {
203
+ return false;
204
+ }
205
+ finally {
206
+ try {
207
+ node_fs_1.default.closeSync(fd);
208
+ }
209
+ catch {
210
+ // already closed — nothing to degrade
211
+ }
212
+ }
213
+ }
214
+ /**
215
+ * Native-UI walk over the shared bounded BFS (#4658): `.xaml` is evidence by
216
+ * extension alone; `.swift`/`.kt`/`.dart` are evidence only when the file's
217
+ * content carries its ecosystem's import marker.
218
+ */
219
+ function treeHasNativeUiFile(projectDir) {
220
+ return walkProjectFiles(projectDir, (name, fullPath) => {
221
+ if (exports.NATIVE_UI_XAML_RE.test(name))
222
+ return true;
223
+ if (exports.NATIVE_UI_SOURCE_RE.test(name)) {
224
+ const markers = exports.NATIVE_UI_CONTENT_MARKERS[node_path_1.default.extname(name).toLowerCase()];
225
+ return markers != null && fileHasAnyMarker(fullPath, markers);
226
+ }
227
+ return false;
228
+ });
229
+ }
143
230
  /**
144
231
  * Does the project tree carry static evidence of a frontend?
145
232
  *
146
233
  * @param projectDir - Absolute path to the project root (the gate's cwd).
147
- * @returns true when package.json declares a UI-framework dependency or the
148
- * tree contains a component-framework file; false otherwise (including
149
- * on any I/O failure — evidence must be affirmative).
234
+ * @returns true when package.json declares a UI-framework dependency, the tree
235
+ * contains a component-framework file, or the tree contains native UI
236
+ * evidence (a `.xaml` file, or a `.swift`/`.kt`/`.dart` file carrying
237
+ * its ecosystem's UI import marker — #4658); false otherwise
238
+ * (including on any I/O failure — evidence must be affirmative).
150
239
  */
151
240
  function hasStaticFrontendEvidence(projectDir) {
152
241
  if (typeof projectDir !== 'string' || projectDir === '')
153
242
  return false;
154
243
  if (packageJsonHasUiFramework(projectDir))
155
244
  return true;
156
- return treeHasComponentFile(projectDir);
245
+ if (treeHasComponentFile(projectDir))
246
+ return true;
247
+ return treeHasNativeUiFile(projectDir);
157
248
  }