@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- package/agents/gsd-code-fixer.compact.md +7 -6
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-debug-session-manager.compact.md +17 -2
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +2 -2
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +13 -8
- package/agents/gsd-intel-updater.compact.md +1 -1
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +8 -7
- package/agents/gsd-planner.md +12 -8
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-ui-auditor.md +155 -17
- package/agents/gsd-ui-researcher.compact.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +10 -9
- package/bin/install.js +642 -95
- package/commands/gsd/autonomous.md +2 -2
- package/commands/gsd/capture.md +1 -1
- package/commands/gsd/mempalace-capture.md +7 -3
- package/commands/gsd/plan-review-convergence.md +6 -6
- package/commands/gsd/progress.md +1 -1
- package/commands/gsd/quick-batch.md +1 -1
- package/commands/gsd/review.md +2 -3
- package/gsd-core/bin/gsd-tools.cjs +335 -22
- package/gsd-core/bin/lib/adr-parser.cjs +3 -1
- package/gsd-core/bin/lib/audit.cjs +81 -13
- package/gsd-core/bin/lib/capability-registry.cjs +82 -187
- package/gsd-core/bin/lib/capability-validator.cjs +0 -1
- package/gsd-core/bin/lib/check-command-router.cjs +101 -14
- package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
- package/gsd-core/bin/lib/commands.cjs +175 -42
- package/gsd-core/bin/lib/config-loader.cjs +65 -4
- package/gsd-core/bin/lib/config.cjs +33 -7
- package/gsd-core/bin/lib/decisions.cjs +30 -14
- package/gsd-core/bin/lib/frontmatter.cjs +13 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +207 -41
- package/gsd-core/bin/lib/install-engine.cjs +13 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
- package/gsd-core/bin/lib/milestone.cjs +18 -5
- package/gsd-core/bin/lib/model-resolver.cjs +159 -50
- package/gsd-core/bin/lib/phase-command-router.cjs +9 -1
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +109 -7
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase.cjs +227 -26
- package/gsd-core/bin/lib/plan-document.cjs +49 -1
- package/gsd-core/bin/lib/planning-document.cjs +459 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +18 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +8 -3
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +163 -3
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
- package/gsd-core/bin/lib/roadmap.cjs +251 -31
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +283 -31
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -33
- package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
- package/gsd-core/bin/lib/state-transition.cjs +39 -2
- package/gsd-core/bin/lib/state.cjs +42 -0
- package/gsd-core/bin/lib/surface.cjs +17 -1
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
- package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
- package/gsd-core/bin/lib/uat.cjs +8 -0
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- package/gsd-core/bin/lib/verification.cjs +268 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
- package/gsd-core/bin/lib/verify.cjs +132 -25
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
- package/gsd-core/bin/shared/config-defaults.manifest.json +3 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +11 -2
- package/gsd-core/references/tdd.md +27 -4
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/worktree-path-safety.md +321 -0
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +2 -2
- package/gsd-core/workflows/add-todo.md +3 -3
- package/gsd-core/workflows/ai-integration-phase.md +11 -3
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +16 -6
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +108 -22
- package/gsd-core/workflows/code-review.md +63 -46
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +1 -1
- package/gsd-core/workflows/complete-milestone.md +2 -2
- package/gsd-core/workflows/debug.md +3 -3
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update.md +3 -3
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +10 -3
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +3 -3
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +36 -26
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +3 -3
- package/gsd-core/workflows/extract-learnings.md +2 -1
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +2 -2
- package/gsd-core/workflows/help/modes/full.compact.md +3 -3
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/import.md +2 -2
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +2 -2
- package/gsd-core/workflows/map-codebase.md +2 -2
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +2 -2
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +7 -7
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +24 -7
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +113 -13
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +1 -1
- package/gsd-core/workflows/quick.md +21 -9
- package/gsd-core/workflows/reapply-patches.md +9 -3
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +31 -16
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -2
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +4 -4
- package/gsd-core/workflows/ship.md +3 -2
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +44 -8
- package/gsd-core/workflows/ui-review.md +18 -4
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +7 -7
- package/gsd-core/workflows/validate-phase.md +3 -2
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +81 -16
- package/hooks/dist/gsd-agent-isolation-guard.js +24 -0
- package/hooks/dist/gsd-secret-read-guard.js +27 -1
- package/hooks/dist/gsd-statusline.js +70 -13
- package/hooks/dist/gsd-validate-commit.sh +63 -4
- package/hooks/gsd-agent-isolation-guard.js +24 -0
- package/hooks/gsd-secret-read-guard.js +27 -1
- package/hooks/gsd-statusline.js +70 -13
- package/hooks/gsd-validate-commit.sh +63 -4
- package/package.json +3 -2
- package/scripts/build-hooks.js +15 -6
- package/scripts/check-contract-drift.cjs +127 -11
- package/scripts/command-contract-helpers.cjs +3 -0
- package/scripts/docs-guard-registry.cjs +28 -0
- package/scripts/gen-loop-host-contract.cjs +69 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +14 -0
- package/scripts/lib/ndjson-reporter.cjs +3 -2
- package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +28 -1
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-id-drift.cjs +290 -5
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +28 -9
- package/scripts/lint-workflow-shellcheck-baseline.json +15 -0
- package/scripts/prompt-injection-scan.sh +4 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/skills/gsd-autonomous/SKILL.md +2 -2
- package/skills/gsd-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +7 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
- package/skills/gsd-progress/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +1 -1
- package/skills/gsd-review/SKILL.md +2 -3
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/gsd-core/bin/lib/uat.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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() &&
|
|
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
|
|
148
|
-
*
|
|
149
|
-
*
|
|
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
|
-
|
|
245
|
+
if (treeHasComponentFile(projectDir))
|
|
246
|
+
return true;
|
|
247
|
+
return treeHasNativeUiFile(projectDir);
|
|
157
248
|
}
|