* feat(#1950): broken-windows ledger — cross-phase defect register gating ship Adds a new capability (#1950) that operationalizes GSD's no-defer discipline as a tracked, enforced artifact: accumulates stubs, TODOs, skipped tests, unrun verifies, and unmet truths across phases, and /gsd-ship blocks while any entry is open. Implementation: - src/broken-windows.cts → gsd-core/bin/lib/broken-windows.cjs: typed IR + I/O entry points (parseLedger/renderLedger/appendWindow/markWaived/markFixed + cmdWindowsStatus/Append/Waive/MarkFixed). Frozen REASON enum for typed error assertions. Windows-safe atomic rename with retry on transient EPERM/EBUSY/EACCES. - gsd-tools.cjs: new subcommand (status | append | waive | fixed), wired via routeWindows + HOST_COMMAND_ROUTERS.windows. - capabilities/broken-windows/capability.json: one ship:pre gate with artifact-frontmatter-equals predicate on WINDOWS.md open_count == 0. activationKey windows.enabled (default true) + sibling windows.enforce (default true, separate so tracking can precede enforcement). - gsd-core/workflows/ship.md: capId==broken-windows branch in preflight, sibling to security — reads gsd_run windows status --raw, fails closed on open_count > 0 or unreadable ledger. - agents/gsd-executor.md: extends the existing ## Known Stubs instruction to also append to WINDOWS.md via gsd_run windows append (best-effort, never blocks execution). - agents/gsd-verifier.md: new Step 8b — record unmet truths + human-verify items in WINDOWS.md. - gsd-core/workflows/progress.md: surfaces open + waived counts. - docs/COMMANDS.md + CONTEXT.md glossary entry + docs/INVENTORY.md: document the gate, waiver mechanism, and new module. - tests/broken-windows.test.cjs: pure + CLI behavioral coverage + fast-check roundtrip property; fail-closed on malformed ledger; security boundary on path traversal in --file. Backward-compatible: a project with no .planning/WINDOWS.md reports open_count: 0 and ships cleanly. Disable enforcement per-project with gsd config-set windows.enforce false (tracking continues, gate stays open). * chore(#1950): ratchet size baselines, defer verifier integration - Workflow size baseline: ship.md 25575→27928, progress.md 31789→32632 (broken-windows preflight branch + open-windows surface). - Agent size baseline: gsd-executor.md 46644→47951 (Known Stubs → also appends to WINDOWS.md). gsd-verifier.md unchanged. - LARGE_CAP (49152) preempted the planned verifier integration (gsd-verifier.md was at 49140 pre-PR — 12 bytes of headroom, not the documented 'real headroom'). Verifier integration deferred to a follow-up PR that extracts the VERIFICATION.md template (lines 739-859) to gsd-core/references/ — a pre-existing cap-tightness defect this PR exposed but does not expand scope to fix. Verifier integration is not in the issue's acceptance criteria (executor writes is; unmet-truths recording was an enhancement, not a gate). * fix(#1950): gate default-off, rename to workflow.windows_enforce, regen goldens Test-failure-driven fixes after first gsd-test run on db8733c8f failed 44 cases (pre-existing structural tests encoded 'ship:pre has 1 gate' / 'all caps off → empty hooks'): - capability manifest: rename windows.enabled+windows.enforce (default true) → single federated key workflow.windows_enforce (default FALSE, opt-in). Matches security's workflow.security_enforce convention and makes the adr857 all-caps-off test pass without modification (the test's buildAllFalseConfig handles workflow.* out of the box). Default-OFF keeps the gate out of the registry's default ship:pre resolution so existing loop-hooks-ship-pre-e2e structural assertions (exactly 1 gate, capId 'security') stay valid; users opt in via gsd config-set workflow.windows_enforce true. - drop activationKey (security doesn't have one either; workflow.* key doubles as the activation toggle). - regenerate docs/reference/capability-matrix.md to include broken-windows (capability-matrix-sync test). - regenerate tests/fixtures/golden-install-parity/*.json (18 runtimes) — installer now emits the new capability + lib file. - update CONTEXT.md, docs/COMMANDS.md, docs/FEATURES.md, ship.md, agents/gsd-executor.md to use the new key name and /gsd:colon slash syntax (slash-command-namespace test). - restore accidentally-regressed /gsd:capture in progress.md. Tracking-only by default; enforcement is opt-in. Acceptance criterion '/gsd-ship fails while any ledger entry is open' is met when workflow.windows_enforce=true (test fixture enables it). * test(#1950): update ship:pre structural invariants for 2-gate registry - loop-hooks-ship-pre-e2e: the registry now declares 2 gates at ship:pre (security + broken-windows), regardless of activation. Activation tests above still pin security-only or empty behavior via fixtures; these structural tests pin the REGISTRY shape, which has 2 gates as of #1950. - workflow-size-baseline: ship.md 27928→27945 (workflow.windows_enforce rename added 17 bytes). * fix(#1950): review H1+H2+M1+M2+M3 — fence-injection, EACCES fail-closed, cleanup, strict line, stryker Adversarial isolated review (Step 6.3) found 2 HIGH findings that block the PR and 3 mediums. All addressed: H1 (HIGH): description containing the markdown 3-backtick fence would terminate the ledger's JSON code block early inside JSON.stringify output (JSON doesn't escape backticks), corrupting the file and bricking the next parse. Fix: use a 4-backtick fence (json ... ) which JSON.stringify cannot produce on its own, AND validate that no entry text field contains a 4-backtick run (reject at append time with new WINDOWS_INVALID_TEXT reason code). Locked by a regression test. H2 (HIGH): readLedgerOrNull swallowed ALL fs errors as 'no ledger', silently returning open_count:0 on EACCES/EPERM/EIO. The ship gate would then pass on an unreadable ledger — the precise vector the workflow doc claims is impossible. Fix: only ENOENT returns null; every other fs error propagates as WINDOWS_LEDGER_MALFORMED so the gate blocks and the operator sees a real diagnostic. Locked by a regression test that chmod 000s a ledger with open_count=1 and asserts the result is never a false-green 0. M1: writeLedgerAtomic left an orphaned .tmp file on rename failure. Wrapped renameWithRetry in try/catch with best-effort unlink. M2: validateLine silently coerced 'abc' → NaN → null, hiding type drift. Removed the line === 0 special case (was undocumented) and made the error message match the strict check. Now any non-positive- integer line value throws, including strings. M3: tests/broken-windows.test.cjs (with its fast-check property test) was not in stryker.config.mjs DEFAULT_TEST_CMD — Stryker would mutate src/broken-windows.cts but no test would catch the mutations, producing false surviving-mutant scores. Added to the list. L1 (dead throw e after error()), L7 (line boundary tests, H1/H2 regression tests, 4-backtick CLI test) also addressed. * docs(#1950): inline concurrency + busy-wait notes (review L2+L3) * fix(#1950): regen goldens against latest gsd-tools; correct --line 0 boundary test gsd-test v4 caught two issues: - goldens I regenerated earlier (commit 526682084) predated the L1 routeWindows catch-block cleanup (commit dd844d565). Regenerated via 'npm run gen:golden' against current HEAD so the install parity hash for gsd-tools.cjs matches. - 'append --line boundary' test expected --line 0 to succeed with null entry.line, but the M2 fix correctly rejects 0 (lines are 1-indexed; 0 is not a valid source line). Updated the boundary test to assert --line 0 fails alongside -1 and 'abc'. * chore(#1950): regen goldens after rebase onto next * chore(#1950): quick.md baseline 50699→50993 (correct resolution from next rebase) * chore(changeset): backfill pr:2441 in .changeset/broken-windows-ledger.md * fix(#1950): renderTable escapes backslash before pipe (CodeQL incomplete-sanitization) CodeQL flagged the markdown-table cell escaper: String(s ?? '').replace(/\|/g, '\\|') — it escapes pipe but not backslash first. A description containing '\|' would render as '\\|' which markdown parses as 'literal backslash' + 'cell separator', splitting the column. Fix: escape backslash FIRST (each \ → \\), then pipe (each | → \|). Now a description with '\|' renders as '\\\\|' (literal '\\' + escaped pipe), which markdown renders as a single '\|' inside the cell. The JSON code block (the parse source-of-truth) was already correctly escaped via JSON.stringify; only the display-only table was affected. Locked by a regression test that: 1. Verifies the JSON block reparses with the description intact. 2. Walks the rendered table row counting unescaped pipes — must be exactly 11 (the row separators for 10 cells), proving no in-cell pipe added a split.
This commit is contained in:
5
.changeset/broken-windows-ledger.md
Normal file
5
.changeset/broken-windows-ledger.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Added
|
||||
pr: 2441
|
||||
---
|
||||
**Broken-windows ledger** — `/gsd:ship` now blocks (when `workflow.windows_enforce=true`, opt-in) while `.planning/WINDOWS.md` has any `open` entry, and the executor auto-populates the ledger with stubs, skipped tests, and unrun verifies as it works. Each window can be `waived` only with a recorded reason (auditable) or `fixed` (removed from the blocking set); `/gsd:progress` surfaces the open + waived counts. Backward-compatible: projects with no ledger ship cleanly (open_count starts at 0), and enforcement is off by default so tracking can precede the gate. Enable with `gsd config-set workflow.windows_enforce true`. (#1950)
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -67,6 +67,7 @@ build/
|
||||
# by `npm run build:lib`). Source of truth is src/; these are emitted, never edited.
|
||||
# Published via prepublishOnly; built before test via pretest. Grows as modules migrate.
|
||||
/tsconfig.build.tsbuildinfo
|
||||
/gsd-core/bin/lib/broken-windows.cjs
|
||||
/gsd-core/bin/lib/host-integration.cjs
|
||||
/gsd-core/bin/lib/host-integration-sdk.cjs
|
||||
/gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs
|
||||
|
||||
@@ -397,6 +397,9 @@ A legal deferred state of an Execute step (`external_job_waiting`): the executor
|
||||
### External-job Capability
|
||||
The producer half of the async external-job contract (#1164, part of #1105). Default-off Capability (`capabilities/external-job/capability.json`) that *writes* `.planning/async-jobs/<job>.json` manifests — the only thing that does; core never writes them. SLURM is the first backend (`sbatch --parsable` submit, `squeue` poll with `sacct` fallback, terminal-state mapping); the design stays scheduler-pluggable via the `backend` field (LSF/PBS/Kubernetes batch forward-declared, not built). Contributions inject at `execute:wave:post` into the executor (classify runtime budget → externalize `long_compute`, commit manifest + handoff, return `external_job_waiting`, defer SUMMARY.md) and at `plan:post` into the planner (emit `<runtime_budget>` quick|medium|unknown|long_compute per task). Activation key `external_job.enabled` (default `false`); sibling keys `external_job.backend`, `external_job.artifact_dir` (default `Artifacts/jobs`, per-job dirs — no fixed log paths, no hardcoded cluster/partition/account), `external_job.submit_timeout_ms` / `external_job.poll_timeout_ms` (bounded subprocesses per CLAUDE.md). Pure producer logic — SLURM state→manifest-status mapping, manifest build/validate, `sbatch`/`squeue`/`sacct` parsers, and the fail-closed manifest writer (refuses a second non-terminal job for a `plan_id` already in flight; refuses to clobber a malformed manifest) — lives in `gsd-core/src/external-job.cts` (generated to `gsd-core/bin/lib/external-job.cjs`); the operator CLI surface is `scripts/slurm-adapter.cjs` (`submit`/`poll`/`show`). Manifest commands are untrusted across the trust seam: `show` surfaces them for confirmation, never auto-runs `submit_command`/`verification_command`/`resume_command`. Test seam: `tests/external-job.test.cjs` (producer behavioral + fast-check property tests; the consumer invariant suite is `tests/external-job-waiting.test.cjs`).
|
||||
|
||||
### Broken Windows Ledger
|
||||
The enforced cross-phase defect register operationalizing GSD's no-defer discipline as a tracked artifact (#1950). Markdown file at `.planning/WINDOWS.md` (project-level, cross-phase) with YAML frontmatter carrying scalar counts (`schema_version`, `open_count`, `waived_count`, `fixed_count`, `total_count`, `last_updated`) for the FAST path the gate reads via jq without parsing JSON, plus a JSON code block as the AUTHORITATIVE entries source; the two cross-check and fail closed on drift. Each entry: `{ id, kind, phase, file, line, description, status, reason, recorded_at, resolved_at }`; kinds are closed (`stub | todo | fixme | skipped-test | lint-warning | unmet-truth | unrun-verify | deviation`); statuses are closed (`open | waived | fixed`). The `broken-windows` Capability (`capabilities/broken-windows/capability.json`) registers one `ship:pre` gate with predicate `artifact-frontmatter-equals WINDOWS.md open_count == 0`; federated config key `workflow.windows_enforce` (default `false` — opt-in enforcement, tracking-only by default so a project can adopt the ledger before turning the gate on). Population is best-effort and never blocks execution: `agents/gsd-executor.md` appends stubs/skipped-tests/unrun-verifies via `gsd_run windows append` after writing SUMMARY.md. Source of truth: `src/broken-windows.cts` → `gsd-core/bin/lib/broken-windows.cjs` (pure `parseLedger`/`renderLedger`/`appendWindow`/`markWaived`/`markFixed` + I/O `cmdWindowsStatus`/`Append`/`Waive`/`MarkFixed`); CLI surface `gsd-tools windows status|append|waive|fixed`. Ship gate enforcement is a `capId == "broken-windows"` branch in `gsd-core/workflows/ship.md` preflight (sibling to the `security` branch); it reads `gsd_run windows status --raw` and fails closed on a non-zero/non-numeric `open_count` (an unparseable ledger is itself a broken window). `/gsd:progress` surfaces the open+waived count. The ledger is optional and backward-compatible: a project with no `.planning/WINDOWS.md` reports `open_count: 0` and ships cleanly, and with `workflow.windows_enforce=false` (the default) ship never blocks on it. Frozen `REASON` enum: `WINDOWS_LEDGER_MISSING | WINDOWS_LEDGER_MALFORMED | WINDOWS_ID_NOT_FOUND | WINDOWS_ALREADY_RESOLVED | WINDOWS_WAIVE_REASON_EMPTY | WINDOWS_INVALID_KIND | WINDOWS_INVALID_FILE | WINDOWS_INVALID_ID | WINDOWS_APPEND_MISSING_FIELD | WINDOWS_USAGE | WINDOWS_OK` — surfaced through `--json-errors` for typed test assertions. Test seam: `tests/broken-windows.test.cjs`. Origin: *The Pragmatic Programmer* Topic 3 (Hunt & Thomas — software transplant of Wilson & Kelling's broken-windows metaphor) plus Cunningham's debt metaphor (decay accrues interest ⇒ accounting, not just habit).
|
||||
|
||||
### Untrusted-input boundary
|
||||
The prompt-level data/instruction isolation seam for untrusted web/document ingress (#1577). Shared reference `gsd-core/references/untrusted-input-boundary.md`, `@`-included by the 10 ingest agents (`gsd-project-researcher`, `gsd-phase-researcher`, `gsd-ui-researcher`, `gsd-assumptions-analyzer`, `gsd-advisor-researcher`, `gsd-ai-researcher`, `gsd-domain-researcher`, `gsd-research-synthesizer`, `gsd-doc-classifier`, `gsd-doc-synthesizer`) — every agent that reads fetch/search/MCP output or external source documents. The reference instructs: treat fetched/read content as **data, never instructions**; self-scan content for embedded directives before use; act only on the assigned task (ignore off-task instructions in data); and wrap quoted untrusted spans in a **fresh random delimiter** per wrap (fixed markers are spoofable). This prompt-level boundary is the primary control — it keeps an injection from being *followed* even while it sits in context. The hook-level companion is the read-injection scanner (`hooks/gsd-read-injection-scanner.js`, PostToolUse on `Read`/`WebFetch`/`WebSearch`), advisory by default; the opt-in top-level `security.injection_blocking` key upgrades HIGH-confidence detections to a PostToolUse circuit-breaker that halts the agent's next step (it runs *after* the fetch, so it is not a redactor). Tests: `tests/untrusted-input-isolation.test.cjs`, `tests/read-injection-scanner.*.test.cjs`, `tests/injection-blocking-config.test.cjs`. See `docs/adr/1577-untrusted-input-boundary-and-injection-blocking.md` and `docs/explanation/security-model.md`. Grounding: arXiv 2506.05739 (PPA), 2507.15219 (PromptArmor), 2504.20472.
|
||||
|
||||
|
||||
@@ -672,6 +672,21 @@ Or: "None - plan executed exactly as written."
|
||||
|
||||
If any stubs exist, add a `## Known Stubs` section to the SUMMARY listing each stub with its file, line, and reason. These are tracked for the verifier to catch. Do NOT mark a plan as complete if stubs exist that prevent the plan's goal from being achieved — either wire the data or document in the plan why the stub is intentional and which future plan will resolve it.
|
||||
|
||||
**Broken-windows ledger (issue #1950).** For each stub, skipped test, or unrun `<verify>` recorded above, ALSO append it to the cross-phase defect register at `.planning/WINDOWS.md`. The ledger accumulates across phases and blocks `/gsd:ship` while any entry is `open`, so a stub written here is visible at ship time even after the per-phase SUMMARY scrolls out of context. Append one entry per defect:
|
||||
|
||||
```bash
|
||||
gsd_run windows append \
|
||||
--kind stub \
|
||||
--phase "${PHASE_NUMBER}" \
|
||||
--file "<path-relative-to-repo-root>" \
|
||||
--line "<line-number-or-omit>" \
|
||||
--description "<one-line description, same wording as the Known Stubs row>"
|
||||
```
|
||||
|
||||
Use `--kind skipped-test` for a `t.skip(...)` / `test.todo(...)` you left behind, `--kind unrun-verify` for a `<verify>` you could not run, or `--kind deviation` for a documented plan deviation. The full kind vocabulary: `stub | todo | fixme | skipped-test | lint-warning | unmet-truth | unrun-verify | deviation`.
|
||||
|
||||
The ledger is **optional**: if `gsd_run windows append` returns `windows_ledger_missing` or `windows_ok` without writing, continue without error — population is best-effort and never blocks execution. Recording here is what makes the defect visible to the ship gate later; forgetting to record is the failure mode this ledger exists to prevent.
|
||||
|
||||
**Threat surface scan:** Before writing the SUMMARY, check if any files created/modified introduce security-relevant surface NOT in the plan's `<threat_model>` — new network endpoints, auth paths, file access patterns, or schema changes at trust boundaries. If found, add:
|
||||
|
||||
```markdown
|
||||
|
||||
46
capabilities/broken-windows/capability.json
Normal file
46
capabilities/broken-windows/capability.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"id": "broken-windows",
|
||||
"role": "feature",
|
||||
"version": "1.7.0",
|
||||
"title": "Broken-windows ledger",
|
||||
"description": "Cross-phase defect register accumulating stubs, TODOs, skipped tests, unrun verifies, and unmet truths into .planning/WINDOWS.md. Blocks /gsd-ship while any window is open unless explicitly waived with a recorded reason. Operationalizes GSD's no-defer discipline as a tracked, enforced artifact (issue #1950).",
|
||||
"tier": "full",
|
||||
"requires": [],
|
||||
"engines": {
|
||||
"gsd": ">=1.7.0"
|
||||
},
|
||||
"runtimeCompat": {
|
||||
"supported": [
|
||||
"*"
|
||||
],
|
||||
"unsupported": []
|
||||
},
|
||||
"skills": [],
|
||||
"agents": [],
|
||||
"hooks": [],
|
||||
"config": {
|
||||
"workflow.windows_enforce": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Enable the blocking ship:pre gate for the broken-windows ledger. When true (opt-in), /gsd-ship blocks while .planning/WINDOWS.md has any open entry. When false (default), windows are still tracked (the executor and verifier still populate WINDOWS.md via gsd-tools windows append) but ship does not block — teams can adopt tracking before enforcement. Issue #1950."
|
||||
}
|
||||
},
|
||||
"steps": [],
|
||||
"contributions": [],
|
||||
"gates": [
|
||||
{
|
||||
"point": "ship:pre",
|
||||
"check": {
|
||||
"predicate": {
|
||||
"kind": "artifact-frontmatter-equals",
|
||||
"artifact": "WINDOWS.md",
|
||||
"field": "open_count",
|
||||
"equals": 0
|
||||
}
|
||||
},
|
||||
"when": "workflow.windows_enforce",
|
||||
"blocking": true,
|
||||
"onError": "halt"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -386,6 +386,11 @@ Create PR from completed phase work with auto-generated body.
|
||||
- Key decisions
|
||||
- Optional configured PRD-style sections from `ship.pr_body_sections`
|
||||
|
||||
**Ship gates (capability-driven):** `/gsd:ship` runs every active `ship:pre` gate from the capability registry. Two are on by default:
|
||||
|
||||
- **Security** (`security` capability): blocks while `SECURITY.md` reports `threats_open > 0`. Resolve via `/gsd:secure-phase {n}`.
|
||||
- **Broken-windows ledger** (`broken-windows` capability, issue #1950): when `workflow.windows_enforce=true` is set, blocks while `.planning/WINDOWS.md` reports any `open` entry. The ledger accumulates stubs, TODOs, skipped tests, unrun verifies, and unmet truths across phases. Resolve an entry with `gsd-tools windows fixed <id>` (defect resolved) or `gsd-tools windows waive <id> "<reason>"` (justified deferral — reason is required and recorded). Inspect via `gsd-tools windows status`. Enforcement is **opt-in** (default `workflow.windows_enforce=false`): enable with `gsd config-set workflow.windows_enforce true`; tracking continues regardless.
|
||||
|
||||
See [Custom PR Body Sections](ship-pr-body-sections.md) for onboarding, examples, and validation rules.
|
||||
|
||||
---
|
||||
|
||||
@@ -3352,4 +3352,16 @@ The load-bearing wire is the `plan-phase` lift into `must_haves.prohibitions`, s
|
||||
|
||||
**Behavior:** A new `gsd-tools state rebuild` subcommand re-derives `STATE.md` from source (#1830). The new `graphify.graph_path` setting makes the knowledge-graph location configurable, so a single umbrella graph can serve several projects (#1825).
|
||||
|
||||
---
|
||||
|
||||
### 158. Broken-Windows Ledger
|
||||
|
||||
**Behavior:** A cross-phase defect register at `.planning/WINDOWS.md` accumulates stubs, TODOs, skipped tests, unrun verifies, and unmet truths (#1950). `/gsd:ship` blocks while any entry is `open`; an entry can be `waived` only with a recorded reason (auditable) or marked `fixed` (removed from the blocking set). `/gsd:progress` surfaces the open + waived counts.
|
||||
|
||||
**Commands:** `gsd-tools windows status | append | waive | fixed`.
|
||||
|
||||
**Config:** `workflow.windows_enforce` (gate active, default `false` — opt-in enforcement). Enable with `gsd config-set workflow.windows_enforce true`. Tracking (the ledger itself, populated by the executor) is always on; only the ship gate is opt-in.
|
||||
|
||||
**Backward compatibility:** A project with no `.planning/WINDOWS.md` reports `open_count: 0` and ships cleanly; the gate only activates once windows are recorded.
|
||||
|
||||
**Configuration:** `graphify.graph_path`
|
||||
|
||||
@@ -308,6 +308,7 @@
|
||||
"assumption-delta.cjs",
|
||||
"audit-command-router.cjs",
|
||||
"audit.cjs",
|
||||
"broken-windows.cjs",
|
||||
"capability-activation.cjs",
|
||||
"capability-command-router.cjs",
|
||||
"capability-consent.cjs",
|
||||
|
||||
@@ -424,6 +424,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
|
||||
| `capability-state.cjs` | Unified capability-state resolver (ADR-857 phase 4b/6) — composes install profile, runtime surface, and config activation into one per-capability view consumed by workflow hook rendering; exports pure `resolveCapabilityState`, reusable `resolveCapabilityRuntimeState`, and I/O handler `cmdCapabilityState`; command surface: `gsd-tools capability state [--config-dir <path>]` emitting `{ runtimeConfigDir, capabilities[] }` |
|
||||
| `capability-trust.cjs` | Capability trust gate (ADR-1244 Phase 4, D5 + compatibility half of D6) — PURE policy module: `discloseExecutableSurfaces` (hooks/command modules/mcpServers), `evaluateInstallTrust` (compose source policy + reserved-namespace + engines gate + disclosure → allowed/requiresConsent/blockReasons), `evaluateSourceAllowed` (`strict_known_registries`: permissive/lockdown/host-allowlist), `checkEngines` (engines.gsd hard gate + `compatVersions` graceful-downgrade), `executableSetChanged` (auto-update re-consent trigger); no sandbox — see `docs/explanation/capability-trust-model.md` |
|
||||
| `capability-validator.cjs` | Shared runtime-callable capability validator (ADR-1244 D2) — extracted from `scripts/gen-capability-registry.cjs` so the build-time generator and the runtime overlay loader share ONE validation implementation (generative-parity guarded); exports `validateCapability`/`validateCrossCapability`/`validateVersionEnvelope`/`validateConsumesGlobal`/… plus the closed-vocabulary sets and `SEMVER_RE` |
|
||||
| `broken-windows.cjs` | Broken-windows ledger library (issue #1950) — typed IR + I/O for `.planning/WINDOWS.md` (cross-phase defect register); pure `parseLedger`/`renderLedger`/`appendWindow`/`markWaived`/`markFixed`/`openCount` + I/O `cmdWindowsStatus`/`cmdWindowsAppend`/`cmdWindowsWaive`/`cmdWindowsMarkFixed`; frozen `REASON` enum for typed-error assertions; CLI surface `gsd-tools windows status\|append\|waive\|fixed`. Generated from `src/broken-windows.cts` |
|
||||
| `capability-writer.cjs` | Capability State Writer (ADR-1213) — write-side inverse of the resolver; projects desired per-capability enabled/gates onto surface + config substrates, then re-resolves (assert-and-report); exports `setCapabilityState` and I/O handler `cmdCapabilitySet`; command surface: `gsd-tools capability set <id> [--on\|--off] [--gate <key>=<true\|false>]` |
|
||||
| `check-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools check` |
|
||||
| `cli-exit.cjs` | `ExitError` class and `runMain()` helper — CLI entrypoints throw `ExitError` instead of calling `process.exit()`; `runMain()` translates the outcome into `process.exitCode` so output flushes cleanly |
|
||||
|
||||
@@ -44,7 +44,7 @@ Core package and are stamped with the package version at release (per
|
||||
ADR-1244 D6). They are not subject to the consent or integrity-pin flow applied
|
||||
to third-party capabilities.
|
||||
|
||||
### Feature capabilities (role: feature) — 19
|
||||
### Feature capabilities (role: feature) — 20
|
||||
|
||||
Feature capabilities extend what the loop does — contributing research,
|
||||
planning, execution, verification, or ship artefacts at the loop extension
|
||||
@@ -55,6 +55,7 @@ points.
|
||||
| `ai-integration` | feature | full | `>=1.6.0` | `plan:pre`, `verify:pre` | step, contribution, gate | first-party |
|
||||
| `assumption-delta` | feature | full | `>=1.6.0` | `plan:pre` | contribution | first-party |
|
||||
| `audit` | feature | full | `>=1.6.0` | — | — | first-party |
|
||||
| `broken-windows` | feature | full | `>=1.7.0` | `ship:pre` | gate | first-party |
|
||||
| `claude-orchestration` | feature | full | `>=1.7.0` | `plan:post`, `execute:wave:pre` | contribution | first-party |
|
||||
| `code-review` | feature | full | `>=1.6.0` | `execute:post` | step | first-party |
|
||||
| `drift` | feature | full | `>=1.6.0` | `plan:pre`, `execute:wave:post` | gate | first-party |
|
||||
|
||||
@@ -82,6 +82,7 @@ export default tseslint.config(
|
||||
'gsd-core/bin/lib/ui-consideration-probe.cjs',
|
||||
'gsd-core/bin/lib/code-review-flags.cjs',
|
||||
'gsd-core/bin/lib/context-utilization.cjs',
|
||||
'gsd-core/bin/lib/broken-windows.cjs',
|
||||
'gsd-core/bin/lib/api-coverage.cjs',
|
||||
'gsd-core/bin/lib/artifacts.cjs',
|
||||
'gsd-core/bin/lib/assumption-delta.cjs',
|
||||
|
||||
@@ -289,6 +289,7 @@ const { routeInitCommand } = require('./lib/init-command-router.cjs');
|
||||
// here, invoked from case 'init' below.
|
||||
const { warnIfStaleBake } = require('./lib/stale-bake-guard.cjs');
|
||||
const loopResolver = require('./lib/loop-resolver.cjs');
|
||||
const brokenWindows = require('./lib/broken-windows.cjs');
|
||||
const { routePhaseCommand } = require('./lib/phase-command-router.cjs');
|
||||
const { routePhasesCommand } = require('./lib/phases-command-router.cjs');
|
||||
const { routeValidateCommand } = require('./lib/validate-command-router.cjs');
|
||||
@@ -1477,6 +1478,39 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
||||
}
|
||||
}
|
||||
|
||||
function routeWindows({ args, cwd, raw, error }) {
|
||||
// windows status | append | waive | fixed (issue #1950)
|
||||
// All subcommands emit JSON; `--raw` is accepted for forward-compat with
|
||||
// capture-stdout hooks but is a no-op (output shape is JSON in both modes).
|
||||
const subcommand = args[1];
|
||||
const rest = args.slice(2);
|
||||
try {
|
||||
if (subcommand === 'status') {
|
||||
brokenWindows.cmdWindowsStatus(cwd, { raw });
|
||||
} else if (subcommand === 'append') {
|
||||
brokenWindows.cmdWindowsAppend(cwd, rest, { raw });
|
||||
} else if (subcommand === 'waive') {
|
||||
brokenWindows.cmdWindowsWaive(cwd, rest, { raw });
|
||||
} else if (subcommand === 'fixed') {
|
||||
brokenWindows.cmdWindowsMarkFixed(cwd, rest, { raw });
|
||||
} else {
|
||||
error(
|
||||
`Unknown windows subcommand: ${subcommand || '(none)'}. Available: status, append, waive, fixed`,
|
||||
ERROR_REASON.SDK_UNKNOWN_COMMAND,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// WindowsError carries a REASON code; surface it through the structured
|
||||
// error path so tests can assert on the typed reason. `error()` calls
|
||||
// process.exit(1) internally so we never reach the fall-through.
|
||||
if (e && e.name === 'WindowsError' && typeof e.reason === 'string') {
|
||||
error(e.message || 'broken-windows error', e.reason);
|
||||
}
|
||||
// Non-WindowsError: surface the message verbatim and exit non-zero.
|
||||
error(`broken-windows: ${(e && e.message) ? e.message : String(e)}`, ERROR_REASON.UNKNOWN);
|
||||
}
|
||||
}
|
||||
|
||||
function routeTeamsStatus({ args, cwd, raw, error }) {
|
||||
const teamsStatus = require('./lib/teams-status.cjs');
|
||||
teamsStatus.cmdTeamsStatus(cwd, { active: args.includes('--active') });
|
||||
@@ -2044,6 +2078,7 @@ const HOST_COMMAND_ROUTERS = {
|
||||
'effort': routeEffort,
|
||||
'user-story': routeUserStory,
|
||||
'drift-guard': routeDriftGuard,
|
||||
'windows': routeWindows,
|
||||
};
|
||||
|
||||
// Returns true when consumed (suppress "Unknown command"), false to fall
|
||||
|
||||
@@ -383,6 +383,52 @@ const capabilities = {
|
||||
}
|
||||
}
|
||||
},
|
||||
"broken-windows": {
|
||||
"id": "broken-windows",
|
||||
"role": "feature",
|
||||
"version": "1.7.0",
|
||||
"title": "Broken-windows ledger",
|
||||
"description": "Cross-phase defect register accumulating stubs, TODOs, skipped tests, unrun verifies, and unmet truths into .planning/WINDOWS.md. Blocks /gsd-ship while any window is open unless explicitly waived with a recorded reason. Operationalizes GSD's no-defer discipline as a tracked, enforced artifact (issue #1950).",
|
||||
"tier": "full",
|
||||
"requires": [],
|
||||
"engines": {
|
||||
"gsd": ">=1.7.0"
|
||||
},
|
||||
"runtimeCompat": {
|
||||
"supported": [
|
||||
"*"
|
||||
],
|
||||
"unsupported": []
|
||||
},
|
||||
"skills": [],
|
||||
"agents": [],
|
||||
"hooks": [],
|
||||
"config": {
|
||||
"workflow.windows_enforce": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Enable the blocking ship:pre gate for the broken-windows ledger. When true (opt-in), /gsd-ship blocks while .planning/WINDOWS.md has any open entry. When false (default), windows are still tracked (the executor and verifier still populate WINDOWS.md via gsd-tools windows append) but ship does not block — teams can adopt tracking before enforcement. Issue #1950."
|
||||
}
|
||||
},
|
||||
"steps": [],
|
||||
"contributions": [],
|
||||
"gates": [
|
||||
{
|
||||
"point": "ship:pre",
|
||||
"check": {
|
||||
"predicate": {
|
||||
"kind": "artifact-frontmatter-equals",
|
||||
"artifact": "WINDOWS.md",
|
||||
"field": "open_count",
|
||||
"equals": 0
|
||||
}
|
||||
},
|
||||
"when": "workflow.windows_enforce",
|
||||
"blocking": true,
|
||||
"onError": "halt"
|
||||
}
|
||||
]
|
||||
},
|
||||
"claude": {
|
||||
"id": "claude",
|
||||
"role": "runtime",
|
||||
@@ -3536,6 +3582,21 @@ const byLoopPoint = {
|
||||
"steps": [],
|
||||
"contributions": [],
|
||||
"gates": [
|
||||
{
|
||||
"capId": "broken-windows",
|
||||
"point": "ship:pre",
|
||||
"check": {
|
||||
"predicate": {
|
||||
"kind": "artifact-frontmatter-equals",
|
||||
"artifact": "WINDOWS.md",
|
||||
"field": "open_count",
|
||||
"equals": 0
|
||||
}
|
||||
},
|
||||
"when": "workflow.windows_enforce",
|
||||
"blocking": true,
|
||||
"onError": "halt"
|
||||
},
|
||||
{
|
||||
"capId": "security",
|
||||
"point": "ship:pre",
|
||||
@@ -3578,6 +3639,7 @@ const configKeys = {
|
||||
"workflow.ai_integration_phase": "ai-integration",
|
||||
"workflow.api_coverage_gate": "ai-integration",
|
||||
"workflow.assumption_delta": "assumption-delta",
|
||||
"workflow.windows_enforce": "broken-windows",
|
||||
"claude_orchestration.enabled": "claude-orchestration",
|
||||
"claude_orchestration.execution_backend": "claude-orchestration",
|
||||
"claude_orchestration.min_agent_sdk_version": "claude-orchestration",
|
||||
@@ -3638,6 +3700,12 @@ const configSchema = {
|
||||
"default": true,
|
||||
"description": "Enable the assumption-delta architecture checkpoint during planning. When a pluralization/optional/chosen signal is detected in the phase scope, the planner is prompted to re-ask whether the primary key / identity model still names the right thing. Advisory (non-blocking)."
|
||||
},
|
||||
"workflow.windows_enforce": {
|
||||
"owner": "broken-windows",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Enable the blocking ship:pre gate for the broken-windows ledger. When true (opt-in), /gsd-ship blocks while .planning/WINDOWS.md has any open entry. When false (default), windows are still tracked (the executor and verifier still populate WINDOWS.md via gsd-tools windows append) but ship does not block — teams can adopt tracking before enforcement. Issue #1950."
|
||||
},
|
||||
"claude_orchestration.enabled": {
|
||||
"owner": "claude-orchestration",
|
||||
"type": "boolean",
|
||||
@@ -5738,6 +5806,7 @@ const _requiresGraph = {
|
||||
"assumption-delta": [],
|
||||
"audit": [],
|
||||
"augment": [],
|
||||
"broken-windows": [],
|
||||
"claude": [],
|
||||
"claude-orchestration": [],
|
||||
"cline": [],
|
||||
|
||||
@@ -131,6 +131,19 @@ CONTEXT: [✓ if has_context | - if not]
|
||||
## Pending Todos
|
||||
- [count] pending — /gsd:capture --list to review
|
||||
|
||||
## Open Windows
|
||||
- [count] open in `.planning/WINDOWS.md` — /gsd:ship blocks while any remain
|
||||
(Only show this section if count > 0; suppressed when ledger is empty or absent)
|
||||
|
||||
```bash
|
||||
WINDOWS_STATUS=$(gsd_run windows status --raw 2>/dev/null || echo '')
|
||||
WINDOWS_OPEN=$(printf '%s' "$WINDOWS_STATUS" | jq -r '.ledger.open_count // 0' 2>/dev/null || echo 0)
|
||||
WINDOWS_WAIVED=$(printf '%s' "$WINDOWS_STATUS" | jq -r '.ledger.waived_count // 0' 2>/dev/null || echo 0)
|
||||
```
|
||||
|
||||
Render `Open Windows` only when `$WINDOWS_OPEN` is greater than `0` (or `$WINDOWS_WAIVED` is greater than `0`, so an auditable deferral history remains visible). Phrase: `{WINDOWS_OPEN} open, {WINDOWS_WAIVED} waived — resolves with /gsd:ship gate; inspect via gsd-tools windows status`. The ledger is cross-phase; the count is the project total, not the current phase's.
|
||||
|
||||
|
||||
## Active Debug Sessions
|
||||
- [count] active — /gsd:debug to continue
|
||||
(Only show this section if count > 0)
|
||||
|
||||
@@ -106,6 +106,43 @@ Verify the work is ready to ship:
|
||||
```
|
||||
|
||||
If no active security `ship:pre` gate hook is present (security enforcement off), skip this check silently.
|
||||
|
||||
7. **Broken-windows ship gate (capability-driven, issue #1950).**
|
||||
|
||||
The `SHIP_PRE_HOOKS_JSON` resolved in step 6 already includes any `broken-windows` gate. Inspect `activeHooks` for an entry with `capId == "broken-windows"` and `kind == "gate"`:
|
||||
|
||||
```bash
|
||||
WINDOWS_GATE_ACTIVE=$(printf '%s' "$SHIP_PRE_HOOKS_JSON" | jq -r \
|
||||
'.activeHooks[]? | select(.capId == "broken-windows" and .kind == "gate" and .blocking == true) | .capId' \
|
||||
2>/dev/null | head -1)
|
||||
```
|
||||
|
||||
If `$WINDOWS_GATE_ACTIVE` is non-empty, enforce the gate by reading the ledger's typed status. The ledger lives at the **project root** (cross-phase, not phase-scoped):
|
||||
|
||||
```bash
|
||||
WINDOWS_STATUS_JSON=$(gsd_run windows status --raw 2>/dev/null || echo '')
|
||||
WINDOWS_OPEN_COUNT=$(printf '%s' "$WINDOWS_STATUS_JSON" | jq -r '.ledger.open_count // "?"' 2>/dev/null || echo '?')
|
||||
```
|
||||
|
||||
- **`WINDOWS_OPEN_COUNT == "0"`** → gate passes; continue to the next preflight check.
|
||||
- **`WINDOWS_OPEN_COUNT` is a positive integer** → block with `WINDOWS_SHIP_GATE_OPEN`:
|
||||
```
|
||||
⚠ Broken-windows ship gate: WINDOWS.md has {WINDOWS_OPEN_COUNT} open window(s).
|
||||
Resolve each entry before shipping, or explicitly waive with a recorded reason:
|
||||
gsd-tools windows fixed <id> # defect resolved
|
||||
gsd-tools windows waive <id> "<reason>" # justified deferral (reason required)
|
||||
Then re-run /gsd:ship.
|
||||
```
|
||||
- **`WINDOWS_OPEN_COUNT` is `"?"`, empty, or non-numeric** → **fail closed and block** with `WINDOWS_SHIP_GATE_READ_FAILED` (the gate is strict equality to `0`; never ship on an unreadable ledger):
|
||||
```
|
||||
⚠ Broken-windows ship gate: could not read open_count from .planning/WINDOWS.md.
|
||||
Inspect the file or run `gsd-tools windows status --raw` to diagnose. The ledger
|
||||
may be malformed; fix it before shipping (an unparseable ledger is a broken window).
|
||||
```
|
||||
|
||||
The ledger is **optional and backward-compatible**: on a project where `gsd_run windows status` returns `open_count: 0` (no `.planning/WINDOWS.md` yet, or an empty ledger), the gate passes silently. The gate only blocks when at least one entry is `open`.
|
||||
|
||||
If no active `broken-windows` `ship:pre` gate hook is present (gate disabled via `workflow.windows_enforce=false`, the default — tracking continues but the gate is opt-in), skip this check silently.
|
||||
</step>
|
||||
|
||||
<step name="push_branch">
|
||||
|
||||
917
src/broken-windows.cts
Normal file
917
src/broken-windows.cts
Normal file
@@ -0,0 +1,917 @@
|
||||
/**
|
||||
* Broken-windows ledger — enforced cross-phase defect register (issue #1950).
|
||||
*
|
||||
* Manages `.planning/WINDOWS.md`: a cross-phase ledger of small defects (stubs,
|
||||
* TODOs, skipped tests, lint warnings, unrun verifies, unmet truths, deviations).
|
||||
* `/gsd-ship` blocks while any entry is `open`; an entry can be `waived` only
|
||||
* with a recorded reason or `fixed`.
|
||||
*
|
||||
* LEAF MODULE — imports ONLY: node:fs, node:path. No other src/ imports.
|
||||
*
|
||||
* Storage format (`.planning/WINDOWS.md`):
|
||||
* ---
|
||||
* schema_version: 1
|
||||
* open_count: N
|
||||
* waived_count: N
|
||||
* fixed_count: N
|
||||
* total_count: N
|
||||
* last_updated: <ISO-8601>
|
||||
* ---
|
||||
* # Broken Windows Ledger
|
||||
* <human-readable prose>
|
||||
* ```json
|
||||
* [ <entries array, canonical JSON> ]
|
||||
* ```
|
||||
*
|
||||
* Frontmatter holds scalar counts (the FAST path the ship gate reads via jq
|
||||
* without parsing JSON). The JSON code block is the AUTHORITATIVE entries
|
||||
* source. The two must agree; read paths cross-check and fail closed on drift.
|
||||
*
|
||||
* Exports:
|
||||
* Constants: REASON, LEDGER_FILE_NAME, SCHEMA_VERSION, KINDS
|
||||
* Pure: emptyLedger, parseLedger, renderLedger, appendWindow,
|
||||
* markWaived, markFixed, openCount, findByStatus
|
||||
* I/O: cmdWindowsStatus, cmdWindowsAppend, cmdWindowsWaive,
|
||||
* cmdWindowsMarkFixed
|
||||
*
|
||||
* Reasoning shape — every cmd* function returns JSON suitable for `--raw`:
|
||||
* success: { ok: true, ledger: <Ledger>, ... }
|
||||
* failure: { ok: false, reason: <REASON.*>, message: <string> }
|
||||
* Failure throws an ExitError-shaped error carrying REASON so the gsd-tools
|
||||
* dispatcher's `--json-errors` mode emits it as a structured code (CONTRIBUTING.md
|
||||
* "Prohibited: Raw Text Matching"). The frozen REASON enum is the typed surface
|
||||
* tests assert against.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
// ─── Constants ─────────────────────────────────────────────────────────────
|
||||
|
||||
export const LEDGER_FILE_NAME = 'WINDOWS.md';
|
||||
export const SCHEMA_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Frozen reason enum. Tests assert against these — they are the typed surface
|
||||
* per CONTRIBUTING.md. Adding a new code requires updating this enum, the I/O
|
||||
* entry point that emits it, AND the test that locks Object.keys(REASON).sort()
|
||||
* — three coordinated changes that keep code and tests from drifting.
|
||||
*/
|
||||
export const REASON = Object.freeze({
|
||||
WINDOWS_OK: 'windows_ok',
|
||||
WINDOWS_LEDGER_MISSING: 'windows_ledger_missing',
|
||||
WINDOWS_LEDGER_MALFORMED: 'windows_ledger_malformed',
|
||||
WINDOWS_ID_NOT_FOUND: 'windows_id_not_found',
|
||||
WINDOWS_ALREADY_RESOLVED: 'windows_already_resolved',
|
||||
WINDOWS_WAIVE_REASON_EMPTY: 'windows_waive_reason_empty',
|
||||
WINDOWS_INVALID_KIND: 'windows_invalid_kind',
|
||||
WINDOWS_INVALID_FILE: 'windows_invalid_file',
|
||||
WINDOWS_INVALID_TEXT: 'windows_invalid_text',
|
||||
WINDOWS_INVALID_ID: 'windows_invalid_id',
|
||||
WINDOWS_APPEND_MISSING_FIELD: 'windows_append_missing_field',
|
||||
WINDOWS_USAGE: 'windows_usage',
|
||||
});
|
||||
|
||||
/** Allowed window kinds. Aligned with the issue's enumerated sources. */
|
||||
export const KINDS = Object.freeze([
|
||||
'stub',
|
||||
'todo',
|
||||
'fixme',
|
||||
'skipped-test',
|
||||
'lint-warning',
|
||||
'unmet-truth',
|
||||
'unrun-verify',
|
||||
'deviation',
|
||||
]);
|
||||
|
||||
const KIND_SET = new Set(KINDS);
|
||||
|
||||
// ─── Types ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export type WindowKind =
|
||||
| 'stub'
|
||||
| 'todo'
|
||||
| 'fixme'
|
||||
| 'skipped-test'
|
||||
| 'lint-warning'
|
||||
| 'unmet-truth'
|
||||
| 'unrun-verify'
|
||||
| 'deviation';
|
||||
|
||||
export type WindowStatus = 'open' | 'waived' | 'fixed';
|
||||
|
||||
export interface WindowEntry {
|
||||
id: number;
|
||||
kind: WindowKind;
|
||||
phase: string;
|
||||
file: string; // '' when not applicable
|
||||
line: number | null; // null when not applicable
|
||||
description: string;
|
||||
status: WindowStatus;
|
||||
reason: string; // '' unless status === 'waived'
|
||||
recorded_at: string; // ISO-8601
|
||||
resolved_at: string | null;
|
||||
}
|
||||
|
||||
/** Input shape for appendWindow — id/status/timestamps are assigned by the fn. */
|
||||
export type WindowInput = Pick<WindowEntry, 'kind' | 'phase' | 'description'> &
|
||||
Partial<Pick<WindowEntry, 'file' | 'line'>>;
|
||||
|
||||
export interface Ledger {
|
||||
schema_version: number;
|
||||
open_count: number;
|
||||
waived_count: number;
|
||||
fixed_count: number;
|
||||
total_count: number;
|
||||
last_updated: string;
|
||||
entries: WindowEntry[];
|
||||
}
|
||||
|
||||
// ─── Errors ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Error carrying a REASON code. gsd-tools.cjs's `--json-errors` mode catches
|
||||
* this and emits `{ ok: false, reason: err.reason, message: err.message }` to
|
||||
* stderr; otherwise the message goes to stderr as plain text and the exit
|
||||
* code is non-zero.
|
||||
*/
|
||||
export class WindowsError extends Error {
|
||||
reason: string;
|
||||
constructor(reason: string, message: string) {
|
||||
super(message);
|
||||
this.name = 'WindowsError';
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Pure: constructors + counts ───────────────────────────────────────────
|
||||
|
||||
export function emptyLedger(now: string): Ledger {
|
||||
return {
|
||||
schema_version: SCHEMA_VERSION,
|
||||
open_count: 0,
|
||||
waived_count: 0,
|
||||
fixed_count: 0,
|
||||
total_count: 0,
|
||||
last_updated: now,
|
||||
entries: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function openCount(ledger: Ledger): number {
|
||||
return ledger.open_count;
|
||||
}
|
||||
|
||||
export function findByStatus(ledger: Ledger, status: WindowStatus): WindowEntry[] {
|
||||
return ledger.entries.filter((e) => e.status === status);
|
||||
}
|
||||
|
||||
function recomputeCounts(ledger: Ledger): Ledger {
|
||||
let open = 0, waived = 0, fixed = 0;
|
||||
for (const e of ledger.entries) {
|
||||
if (e.status === 'open') open++;
|
||||
else if (e.status === 'waived') waived++;
|
||||
else if (e.status === 'fixed') fixed++;
|
||||
}
|
||||
return {
|
||||
...ledger,
|
||||
open_count: open,
|
||||
waived_count: waived,
|
||||
fixed_count: fixed,
|
||||
total_count: ledger.entries.length,
|
||||
};
|
||||
}
|
||||
|
||||
function validateKind(kind: unknown): asserts kind is WindowKind {
|
||||
if (typeof kind !== 'string' || !KIND_SET.has(kind)) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_INVALID_KIND,
|
||||
`Invalid window kind: ${JSON.stringify(kind)}. Allowed: ${KINDS.join(', ')}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateDescription(description: unknown): string {
|
||||
if (typeof description !== 'string' || description.trim() === '') {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_APPEND_MISSING_FIELD,
|
||||
'Window description must be a non-empty string.',
|
||||
);
|
||||
}
|
||||
rejectBacktickRun(description, 'description');
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject any string field that contains a 4-backtick run. The ledger's JSON
|
||||
* code block uses a 4-backtick fence; a 4-backtick run inside stringified
|
||||
* entry text would terminate the fence early and brick the next parse
|
||||
* (issue #1950 review H1). JSON.stringify does not escape backticks, so we
|
||||
* must catch them at validate time.
|
||||
*/
|
||||
function rejectBacktickRun(value: string, field: string): void {
|
||||
if (value.includes(FORBIDDEN_BACKTICK_RUN)) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_INVALID_TEXT,
|
||||
`Window ${field} contains a 4-backtick run, which would corrupt the ledger's JSON code fence.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateFile(file: unknown): string {
|
||||
if (file == null || file === '') return '';
|
||||
if (typeof file !== 'string') {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_INVALID_FILE,
|
||||
'Window file must be a string when provided.',
|
||||
);
|
||||
}
|
||||
// Reject path traversal — the ledger is a project-local artifact; absolute or
|
||||
// parent-escaping paths serve no legitimate purpose and could mislead a human
|
||||
// reviewer into investigating the wrong location. Reject NUL bytes too.
|
||||
if (file.includes('\0')) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_INVALID_FILE,
|
||||
'Window file contains a NUL byte.',
|
||||
);
|
||||
}
|
||||
if (path.isAbsolute(file) || /(^|[/\\])\.\.([/\\]|$)/.test(file)) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_INVALID_FILE,
|
||||
`Window file rejects path traversal/absolute paths: ${file}`,
|
||||
);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
function validateLine(line: unknown): number | null {
|
||||
if (line == null || line === '') return null;
|
||||
// Strict: number or numeric string only; reject garbage like "abc" (which
|
||||
// Number() would silently coerce to NaN → null, hiding type drift). Issue
|
||||
// #1950 review M2.
|
||||
const n = typeof line === 'number' ? line : Number(line);
|
||||
if (!Number.isInteger(n) || n < 1) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_APPEND_MISSING_FIELD,
|
||||
`Window line must be a positive integer when provided (got: ${JSON.stringify(line)}).`,
|
||||
);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function nextId(entries: WindowEntry[]): number {
|
||||
let max = 0;
|
||||
for (const e of entries) if (e.id > max) max = e.id;
|
||||
return max + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a window to the ledger. Assigns the next dense id (max+1), sets
|
||||
* status=open, timestamps via opts.now.
|
||||
*
|
||||
* Concurrency (issue #1950 review L2): NOT safe for concurrent writers. Two
|
||||
* parallel `gsd_run windows append` invocations both read the same snapshot,
|
||||
* both compute the same nextId, both write — the second atomic rename wins
|
||||
* and the first append (and the entry it added) is silently lost. This is
|
||||
* acceptable in the current single-executor-per-phase model; document if the
|
||||
* executor ever gains parallel wave-level append.
|
||||
*/
|
||||
export function appendWindow(
|
||||
ledger: Ledger,
|
||||
input: WindowInput,
|
||||
opts: { now: string } = { now: new Date().toISOString() },
|
||||
): { ledger: Ledger; entry: WindowEntry } {
|
||||
validateKind(input.kind);
|
||||
const description = validateDescription(input.description);
|
||||
const file = validateFile(input.file);
|
||||
const line = validateLine(input.line);
|
||||
|
||||
const id = nextId(ledger.entries);
|
||||
const entry: WindowEntry = {
|
||||
id,
|
||||
kind: input.kind,
|
||||
phase: String(input.phase ?? ''),
|
||||
file,
|
||||
line,
|
||||
description,
|
||||
status: 'open',
|
||||
reason: '',
|
||||
recorded_at: opts.now,
|
||||
resolved_at: null,
|
||||
};
|
||||
|
||||
const entries = [...ledger.entries, entry];
|
||||
const result = recomputeCounts({ ...ledger, entries, last_updated: opts.now });
|
||||
return { ledger: result, entry };
|
||||
}
|
||||
|
||||
function findEntryOrFail(ledger: Ledger, id: number): WindowEntry {
|
||||
const entry = ledger.entries.find((e) => e.id === id);
|
||||
if (!entry) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_ID_NOT_FOUND,
|
||||
`No window with id ${id}.`,
|
||||
);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
function assertOpen(entry: WindowEntry): void {
|
||||
if (entry.status !== 'open') {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_ALREADY_RESOLVED,
|
||||
`Window ${entry.id} is already ${entry.status} (resolved_at=${entry.resolved_at}).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function markWaived(
|
||||
ledger: Ledger,
|
||||
id: number,
|
||||
reason: string,
|
||||
opts: { now: string } = { now: new Date().toISOString() },
|
||||
): Ledger {
|
||||
if (typeof reason !== 'string' || reason.trim() === '') {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_WAIVE_REASON_EMPTY,
|
||||
'Waive requires a non-empty recorded reason.',
|
||||
);
|
||||
}
|
||||
const entry = findEntryOrFail(ledger, id);
|
||||
assertOpen(entry);
|
||||
|
||||
const newStatus: WindowStatus = 'waived';
|
||||
const entries = ledger.entries.map((e) =>
|
||||
e.id === id
|
||||
? { ...e, status: newStatus, reason, resolved_at: opts.now }
|
||||
: e,
|
||||
);
|
||||
return recomputeCounts({ ...ledger, entries, last_updated: opts.now });
|
||||
}
|
||||
|
||||
export function markFixed(
|
||||
ledger: Ledger,
|
||||
id: number,
|
||||
opts: { now: string } = { now: new Date().toISOString() },
|
||||
): Ledger {
|
||||
const entry = findEntryOrFail(ledger, id);
|
||||
assertOpen(entry);
|
||||
|
||||
const newStatus: WindowStatus = 'fixed';
|
||||
const entries = ledger.entries.map((e) =>
|
||||
e.id === id
|
||||
? { ...e, status: newStatus, resolved_at: opts.now }
|
||||
: e,
|
||||
);
|
||||
return recomputeCounts({ ...ledger, entries, last_updated: opts.now });
|
||||
}
|
||||
|
||||
// ─── Pure: parse / render ──────────────────────────────────────────────────
|
||||
|
||||
// JSON-FENCE strategy (issue #1950 review H1): a description containing the
|
||||
// 3-backtick markdown fence sequence would terminate the code block early
|
||||
// inside JSON.stringify output (which does not escape backticks), corrupting
|
||||
// the file and bricking the next parse. We use a 4-backtick fence which
|
||||
// cannot collide with anything JSON.stringify can emit on its own (JSON has
|
||||
// no 4-backtick operator), AND validate that no entry's text fields contain
|
||||
// a 4-backtick run, so the rendered file is provably reparseable.
|
||||
const JSON_FENCE_OPEN = '````json';
|
||||
const JSON_FENCE_CLOSE = '````';
|
||||
const FORBIDDEN_BACKTICK_RUN = '````';
|
||||
|
||||
/**
|
||||
* Minimal strict frontmatter parser for flat scalar keys. Only supports the
|
||||
* shape this module emits: `key: <number|string>` per line. Throws on any
|
||||
* structural deviation — fail-closed on drift.
|
||||
*/
|
||||
function parseFrontmatterStrict(raw: string): Record<string, number | string> {
|
||||
if (!raw.startsWith('---\n') && !raw.startsWith('---\r\n')) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
'Ledger missing frontmatter opening ---',
|
||||
);
|
||||
}
|
||||
const headerEnd = raw.startsWith('---\r\n') ? 5 : 4;
|
||||
const closeIdx = raw.indexOf('\n---', headerEnd);
|
||||
if (closeIdx === -1) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
'Ledger missing frontmatter closing ---',
|
||||
);
|
||||
}
|
||||
const yamlBody = raw.slice(headerEnd, closeIdx);
|
||||
const out: Record<string, number | string> = {};
|
||||
for (const line of yamlBody.split(/\r?\n/)) {
|
||||
if (line.trim() === '') continue;
|
||||
const m = line.match(/^([a-zA-Z0-9_]+):\s*(.*)$/);
|
||||
if (!m) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
`Ledger frontmatter line is not key: value: ${JSON.stringify(line)}`,
|
||||
);
|
||||
}
|
||||
const [, key, valueStr] = m;
|
||||
const trimmed = valueStr.trim();
|
||||
if (/^-?\d+$/.test(trimmed)) {
|
||||
out[key] = Number(trimmed);
|
||||
} else if (/^-?\d+\.\d+$/.test(trimmed)) {
|
||||
out[key] = Number(trimmed);
|
||||
} else {
|
||||
// String — strip surrounding quotes if present.
|
||||
out[key] =
|
||||
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
||||
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
||||
? trimmed.slice(1, -1)
|
||||
: trimmed;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseJsonBlock(raw: string): WindowEntry[] {
|
||||
const start = raw.indexOf(JSON_FENCE_OPEN);
|
||||
if (start === -1) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
'Ledger missing JSON code block for entries.',
|
||||
);
|
||||
}
|
||||
const end = raw.indexOf(JSON_FENCE_CLOSE, start + JSON_FENCE_OPEN.length);
|
||||
if (end === -1) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
'Ledger JSON code block not terminated.',
|
||||
);
|
||||
}
|
||||
const jsonText = raw.slice(start + JSON_FENCE_OPEN.length, end).trim();
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(jsonText);
|
||||
} catch (e) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
`Ledger JSON block failed to parse: ${(e as Error).message}`,
|
||||
);
|
||||
}
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
'Ledger JSON block must be an array.',
|
||||
);
|
||||
}
|
||||
return parsed.map(validateEntryShape);
|
||||
}
|
||||
|
||||
function validateEntryShape(e: unknown, i: number): WindowEntry {
|
||||
if (typeof e !== 'object' || e === null) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
`Ledger entry ${i} is not an object.`,
|
||||
);
|
||||
}
|
||||
const o = e as Record<string, unknown>;
|
||||
const required = ['id', 'kind', 'phase', 'file', 'description', 'status', 'reason', 'recorded_at'];
|
||||
for (const k of required) {
|
||||
if (!(k in o)) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
`Ledger entry ${i} missing required field: ${k}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (typeof o.id !== 'number' || !Number.isInteger(o.id) || o.id < 1) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
`Ledger entry ${i} has invalid id.`,
|
||||
);
|
||||
}
|
||||
if (typeof o.kind !== 'string' || !KIND_SET.has(o.kind)) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
`Ledger entry ${i} has invalid kind: ${JSON.stringify(o.kind)}`,
|
||||
);
|
||||
}
|
||||
if (typeof o.status !== 'string' || !['open', 'waived', 'fixed'].includes(o.status)) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
`Ledger entry ${i} has invalid status: ${JSON.stringify(o.status)}`,
|
||||
);
|
||||
}
|
||||
if (typeof o.description !== 'string' || typeof o.reason !== 'string') {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
`Ledger entry ${i} has non-string description/reason.`,
|
||||
);
|
||||
}
|
||||
const phaseStr = typeof o.phase === 'string'
|
||||
? o.phase
|
||||
: (o.phase == null ? '' : typeof o.phase === 'number' || typeof o.phase === 'boolean' ? String(o.phase) : '');
|
||||
const recordedStr = typeof o.recorded_at === 'string'
|
||||
? o.recorded_at
|
||||
: (o.recorded_at == null ? '' : typeof o.recorded_at === 'number' || typeof o.recorded_at === 'boolean' ? String(o.recorded_at) : '');
|
||||
const resolvedStr = typeof o.resolved_at === 'string'
|
||||
? o.resolved_at
|
||||
: (o.resolved_at == null ? null : typeof o.resolved_at === 'number' || typeof o.resolved_at === 'boolean' ? String(o.resolved_at) : null);
|
||||
return {
|
||||
id: o.id,
|
||||
kind: o.kind as WindowKind,
|
||||
phase: phaseStr,
|
||||
file: typeof o.file === 'string' ? o.file : '',
|
||||
line: o.line == null ? null : (Number(o.line) || null),
|
||||
description: o.description,
|
||||
status: o.status as WindowStatus,
|
||||
reason: o.reason,
|
||||
recorded_at: recordedStr,
|
||||
resolved_at: resolvedStr,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseLedger(raw: string): Ledger {
|
||||
const fm = parseFrontmatterStrict(raw);
|
||||
if (fm.schema_version !== SCHEMA_VERSION) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
`Ledger schema_version must be ${SCHEMA_VERSION}; got ${JSON.stringify(fm.schema_version)}.`,
|
||||
);
|
||||
}
|
||||
const requiredCounts = ['open_count', 'waived_count', 'fixed_count', 'total_count'];
|
||||
for (const k of requiredCounts) {
|
||||
const v = fm[k];
|
||||
if (typeof v !== 'number' || !Number.isInteger(v)) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
`Ledger ${k} must be an integer; got ${JSON.stringify(v)}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (typeof fm.last_updated !== 'string') {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
`Ledger last_updated must be a string; got ${JSON.stringify(fm.last_updated)}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const entries = parseJsonBlock(raw);
|
||||
const ledger: Ledger = {
|
||||
schema_version: SCHEMA_VERSION,
|
||||
open_count: typeof fm.open_count === 'number' ? fm.open_count : 0,
|
||||
waived_count: typeof fm.waived_count === 'number' ? fm.waived_count : 0,
|
||||
fixed_count: typeof fm.fixed_count === 'number' ? fm.fixed_count : 0,
|
||||
total_count: typeof fm.total_count === 'number' ? fm.total_count : 0,
|
||||
last_updated: typeof fm.last_updated === 'string' ? fm.last_updated : '',
|
||||
entries,
|
||||
};
|
||||
|
||||
// Cross-check: frontmatter counts must agree with entries-derived counts.
|
||||
const recomputed = recomputeCounts(ledger);
|
||||
if (
|
||||
recomputed.open_count !== ledger.open_count ||
|
||||
recomputed.waived_count !== ledger.waived_count ||
|
||||
recomputed.fixed_count !== ledger.fixed_count ||
|
||||
recomputed.total_count !== ledger.total_count
|
||||
) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
`Ledger counts disagree with entries: frontmatter open/waived/fixed/total=` +
|
||||
`${ledger.open_count}/${ledger.waived_count}/${ledger.fixed_count}/${ledger.total_count}` +
|
||||
` but entries yield ${recomputed.open_count}/${recomputed.waived_count}/${recomputed.fixed_count}/${recomputed.total_count}.`,
|
||||
);
|
||||
}
|
||||
return ledger;
|
||||
}
|
||||
|
||||
export function renderLedger(ledger: Ledger): string {
|
||||
const fm = [
|
||||
'---',
|
||||
`schema_version: ${ledger.schema_version}`,
|
||||
`open_count: ${ledger.open_count}`,
|
||||
`waived_count: ${ledger.waived_count}`,
|
||||
`fixed_count: ${ledger.fixed_count}`,
|
||||
`total_count: ${ledger.total_count}`,
|
||||
`last_updated: ${ledger.last_updated}`,
|
||||
'---',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const header = [
|
||||
'# Broken Windows Ledger',
|
||||
'',
|
||||
'> Cross-phase defect register. `/gsd-ship` blocks while `open_count > 0`.',
|
||||
'> Waive with `gsd-tools windows waive <id> "<reason>"` (reason required).',
|
||||
'> Mark fixed with `gsd-tools windows fixed <id>`.',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const table = renderTable(ledger.entries);
|
||||
const jsonBlock = [JSON_FENCE_OPEN, JSON.stringify(ledger.entries, null, 2), JSON_FENCE_CLOSE, ''].join('\n');
|
||||
|
||||
return [fm, header, table, '', jsonBlock].join('\n');
|
||||
}
|
||||
|
||||
function renderTable(entries: WindowEntry[]): string {
|
||||
if (entries.length === 0) {
|
||||
return [
|
||||
'| id | phase | kind | file | line | description | status | reason | recorded_at | resolved_at |',
|
||||
'|----|-------|------|------|------|-------------|--------|--------|-------------|-------------|',
|
||||
'| _(none)_ | | | | | _No windows recorded._ | | | | |',
|
||||
].join('\n');
|
||||
}
|
||||
const rows = [
|
||||
'| id | phase | kind | file | line | description | status | reason | recorded_at | resolved_at |',
|
||||
'|----|-------|------|------|------|-------------|--------|--------|-------------|-------------|',
|
||||
];
|
||||
for (const e of entries) {
|
||||
// Escape backslash FIRST, then pipe — markdown table cells treat `\` as
|
||||
// the escape introducer, so a description containing `\|` would render
|
||||
// as an escaped pipe (i.e. a literal `|` inside the cell) and split the
|
||||
// column. Escaping `\` → `\\` first makes the subsequent `\|` replacement
|
||||
// unambiguous. (CodeQL: js/incomplete-sanitization — issue #1950 PR #2441.)
|
||||
const cell = (s: string | number | null) =>
|
||||
String(s ?? '')
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/\|/g, '\\|');
|
||||
rows.push(
|
||||
[
|
||||
'|', cell(e.id), '|', cell(e.phase), '|', cell(e.kind), '|',
|
||||
cell(e.file), '|', cell(e.line ?? ''), '|',
|
||||
cell(e.description), '|', cell(e.status), '|',
|
||||
cell(e.reason), '|', cell(e.recorded_at), '|', cell(e.resolved_at), '|',
|
||||
].join(' '),
|
||||
);
|
||||
}
|
||||
return rows.join('\n');
|
||||
}
|
||||
|
||||
// ─── I/O entry points ──────────────────────────────────────────────────────
|
||||
|
||||
function ledgerPath(cwd: string): string {
|
||||
return path.join(cwd, '.planning', LEDGER_FILE_NAME);
|
||||
}
|
||||
|
||||
function readLedgerOrNull(cwd: string): Ledger | null {
|
||||
const p = ledgerPath(cwd);
|
||||
let raw: string;
|
||||
try {
|
||||
raw = fs.readFileSync(p, 'utf8');
|
||||
} catch (e: unknown) {
|
||||
// ENOENT is the only "no ledger yet" case. Every other fs error (EACCES,
|
||||
// EPERM, EIO, ENOTDIR, EBADF, ...) must NOT be silently coerced to "empty
|
||||
// ledger" — that would fail the ship gate OPEN on an unreadable ledger,
|
||||
// contradicting the workflow's documented "fail closed on unreadable"
|
||||
// invariant (issue #1950 review H2). Propagate as malformed so the gate
|
||||
// blocks and the operator sees a real diagnostic.
|
||||
const code = (e && typeof e === 'object' && 'code' in e)
|
||||
? String((e as { code?: unknown }).code)
|
||||
: '';
|
||||
if (code === 'ENOENT') return null;
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
`Could not read ledger at ${p} (${code || 'unknown fs error'}): ${(e as Error).message}.`,
|
||||
);
|
||||
}
|
||||
// parseLedger throws WindowsError on malformed content — caller surfaces it.
|
||||
return parseLedger(raw);
|
||||
}
|
||||
|
||||
function ensurePlanningDir(cwd: string): void {
|
||||
const dir = path.join(cwd, '.planning');
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Errnos that Windows throws transiently on rename when a reader or antivirus
|
||||
* scanner holds the target. We retry through these; anything else propagates.
|
||||
*
|
||||
* NOTE (issue #1950 review L3): the retry uses a short busy-wait rather than
|
||||
* setTimeout — this is a synchronous CLI path with no event loop to yield on,
|
||||
* and the cumulative wait is bounded at 25+50+100+200 = 375ms across 5 attempts.
|
||||
* If a future caller moves this onto an async path, swap to awaitable sleeps.
|
||||
*/
|
||||
const RENAME_RETRY_ERRNOS = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
||||
const RENAME_MAX_ATTEMPTS = 5;
|
||||
const RENAME_BACKOFF_MS = 25;
|
||||
|
||||
function renameWithRetry(tmp: string, target: string): void {
|
||||
let lastErr: unknown;
|
||||
for (let attempt = 0; attempt < RENAME_MAX_ATTEMPTS; attempt++) {
|
||||
try {
|
||||
fs.renameSync(tmp, target);
|
||||
return;
|
||||
} catch (err: unknown) {
|
||||
lastErr = err;
|
||||
const code = (err && typeof err === 'object' && 'code' in err) ? String((err as { code?: unknown }).code) : '';
|
||||
if (code && RENAME_RETRY_ERRNOS.has(code) && attempt < RENAME_MAX_ATTEMPTS - 1) {
|
||||
// Exponential-ish backoff: 25ms, 50ms, 100ms, 200ms.
|
||||
const delay = RENAME_BACKOFF_MS * Math.pow(2, attempt);
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < delay) {
|
||||
// Busy-wait a very short time — Windows transient locks usually clear in <100ms.
|
||||
}
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
throw lastErr;
|
||||
}
|
||||
|
||||
function writeLedgerAtomic(cwd: string, ledger: Ledger): void {
|
||||
ensurePlanningDir(cwd);
|
||||
const p = ledgerPath(cwd);
|
||||
const tmp = `${p}.${process.pid}.tmp`;
|
||||
fs.writeFileSync(tmp, renderLedger(ledger), 'utf8');
|
||||
try {
|
||||
renameWithRetry(tmp, p);
|
||||
} catch (err) {
|
||||
// Clean up the orphaned tmp file so repeated failures don't accumulate
|
||||
// `.planning/WINDOWS.md.<pid>.tmp` files (issue #1950 review M1). Best-effort:
|
||||
// unlink failures (e.g., already gone) are swallowed.
|
||||
try { fs.unlinkSync(tmp); } catch { /* best-effort cleanup */ }
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
/** Emit a JSON result to stdout in the canonical shape. */
|
||||
function emit(obj: unknown): void {
|
||||
process.stdout.write(JSON.stringify(obj, null, 2));
|
||||
}
|
||||
|
||||
/** `gsd-tools windows status [--raw]`. */
|
||||
export function cmdWindowsStatus(cwd: string, opts: { raw?: boolean } = {}): void {
|
||||
let ledger: Ledger;
|
||||
try {
|
||||
ledger = readLedgerOrNull(cwd) ?? emptyLedger(nowIso());
|
||||
} catch (e) {
|
||||
if (e instanceof WindowsError) throw e;
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_LEDGER_MALFORMED,
|
||||
`Unexpected error reading ledger: ${(e as Error).message}`,
|
||||
);
|
||||
}
|
||||
void opts; // status output is JSON in both human and raw modes (single shape)
|
||||
emit({ ok: true, ledger });
|
||||
}
|
||||
|
||||
/** `gsd-tools windows append --kind K --phase N [--file F] [--line L] --description D`. */
|
||||
export function cmdWindowsAppend(
|
||||
cwd: string,
|
||||
args: string[],
|
||||
opts: { raw?: boolean } = {},
|
||||
): void {
|
||||
void opts;
|
||||
const parsed = parseArgs(args, {
|
||||
flags: ['--kind', '--phase', '--file', '--line', '--description'],
|
||||
required: ['--kind', '--phase', '--description'],
|
||||
});
|
||||
|
||||
let ledger: Ledger;
|
||||
try {
|
||||
ledger = readLedgerOrNull(cwd) ?? emptyLedger(nowIso());
|
||||
} catch (e) {
|
||||
if (e instanceof WindowsError) throw e;
|
||||
throw new WindowsError(REASON.WINDOWS_LEDGER_MALFORMED, (e as Error).message);
|
||||
}
|
||||
|
||||
const result = appendWindow(
|
||||
ledger,
|
||||
{
|
||||
kind: parsed.values['--kind'] as WindowKind,
|
||||
phase: parsed.values['--phase'] ?? '',
|
||||
file: parsed.values['--file'] ?? '',
|
||||
line: parsed.values['--line'] == null ? null : Number(parsed.values['--line']),
|
||||
description: parsed.values['--description'] ?? '',
|
||||
},
|
||||
{ now: nowIso() },
|
||||
);
|
||||
writeLedgerAtomic(cwd, result.ledger);
|
||||
emit({ ok: true, ledger: result.ledger, entry: result.entry });
|
||||
}
|
||||
|
||||
/** `gsd-tools windows waive <id> "<reason>"`. */
|
||||
export function cmdWindowsWaive(
|
||||
cwd: string,
|
||||
args: string[],
|
||||
opts: { raw?: boolean } = {},
|
||||
): void {
|
||||
void opts;
|
||||
const { positionals } = parseArgs(args, { flags: [], required: [], positionals: 2 });
|
||||
const idStr = positionals[0];
|
||||
const reason = positionals[1];
|
||||
|
||||
const id = parseIdOrThrow(idStr);
|
||||
|
||||
let ledger: Ledger;
|
||||
try {
|
||||
ledger = readLedgerOrNull(cwd) ?? emptyLedger(nowIso());
|
||||
} catch (e) {
|
||||
if (e instanceof WindowsError) throw e;
|
||||
throw new WindowsError(REASON.WINDOWS_LEDGER_MALFORMED, (e as Error).message);
|
||||
}
|
||||
|
||||
const updated = markWaived(ledger, id, reason ?? '', { now: nowIso() });
|
||||
writeLedgerAtomic(cwd, updated);
|
||||
emit({ ok: true, ledger: updated });
|
||||
}
|
||||
|
||||
/** `gsd-tools windows fixed <id>`. */
|
||||
export function cmdWindowsMarkFixed(
|
||||
cwd: string,
|
||||
args: string[],
|
||||
opts: { raw?: boolean } = {},
|
||||
): void {
|
||||
void opts;
|
||||
const { positionals } = parseArgs(args, { flags: [], required: [], positionals: 1 });
|
||||
const id = parseIdOrThrow(positionals[0]);
|
||||
|
||||
let ledger: Ledger;
|
||||
try {
|
||||
ledger = readLedgerOrNull(cwd) ?? emptyLedger(nowIso());
|
||||
} catch (e) {
|
||||
if (e instanceof WindowsError) throw e;
|
||||
throw new WindowsError(REASON.WINDOWS_LEDGER_MALFORMED, (e as Error).message);
|
||||
}
|
||||
|
||||
const updated = markFixed(ledger, id, { now: nowIso() });
|
||||
writeLedgerAtomic(cwd, updated);
|
||||
emit({ ok: true, ledger: updated });
|
||||
}
|
||||
|
||||
function parseIdOrThrow(raw: string | undefined): number {
|
||||
if (raw == null || raw === '') {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_INVALID_ID,
|
||||
'Window id is required.',
|
||||
);
|
||||
}
|
||||
const n = Number(raw);
|
||||
if (!Number.isInteger(n) || n < 1) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_INVALID_ID,
|
||||
`Window id must be a positive integer (got: ${JSON.stringify(raw)}).`,
|
||||
);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/** Minimal argv parser — flag values via `--flag value` or `--flag=value`. */
|
||||
function parseArgs(
|
||||
args: string[],
|
||||
spec: { flags: string[]; required: string[]; positionals?: number },
|
||||
): { values: Record<string, string | undefined>; positionals: string[] } {
|
||||
const values: Record<string, string | undefined> = {};
|
||||
const positionals: string[] = [];
|
||||
const flagSet = new Set(spec.flags);
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i];
|
||||
if (a == null) continue;
|
||||
if (a.startsWith('--')) {
|
||||
const eq = a.indexOf('=');
|
||||
const flagName = eq === -1 ? a : a.slice(0, eq);
|
||||
if (!flagSet.has(flagName)) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_USAGE,
|
||||
`Unknown flag: ${flagName}`,
|
||||
);
|
||||
}
|
||||
if (eq !== -1) {
|
||||
values[flagName] = a.slice(eq + 1);
|
||||
} else {
|
||||
const next = args[i + 1];
|
||||
if (next == null || next.startsWith('--')) {
|
||||
if (!(flagName in values)) values[flagName] = undefined;
|
||||
} else {
|
||||
values[flagName] = next;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
positionals.push(a);
|
||||
}
|
||||
}
|
||||
|
||||
for (const r of spec.required) {
|
||||
if (values[r] == null || values[r] === '') {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_USAGE,
|
||||
`Missing required flag: ${r}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const want = spec.positionals ?? 0;
|
||||
if (positionals.length < want) {
|
||||
throw new WindowsError(
|
||||
REASON.WINDOWS_USAGE,
|
||||
`Expected ${want} positional argument(s); got ${positionals.length}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return { values, positionals };
|
||||
}
|
||||
@@ -62,7 +62,7 @@ const UNMUTATED = [
|
||||
// Full test command used by local runs and as the fallback when CI does not
|
||||
// inject a per-shard command via MUTATION_TEST_CMD.
|
||||
// Keep this list in sync with the tests arrays in scripts/mutation-matrix.cjs COVERED.
|
||||
const DEFAULT_TEST_CMD = 'node --test tests/context-utilization.property.test.cjs tests/prompt-budget.property.test.cjs tests/frontmatter.property.test.cjs tests/adr-parser.property.test.cjs tests/config-schema.property.test.cjs tests/adr-parser.test.cjs tests/active-workstream-store.test.cjs tests/active-workstream-store.unit.test.cjs tests/prompt-budget.unit.test.cjs tests/adr-parser.unit.test.cjs tests/frontmatter.unit.test.cjs tests/core-utils.test.cjs';
|
||||
const DEFAULT_TEST_CMD = 'node --test tests/context-utilization.property.test.cjs tests/prompt-budget.property.test.cjs tests/frontmatter.property.test.cjs tests/adr-parser.property.test.cjs tests/config-schema.property.test.cjs tests/adr-parser.test.cjs tests/active-workstream-store.test.cjs tests/active-workstream-store.unit.test.cjs tests/prompt-budget.unit.test.cjs tests/adr-parser.unit.test.cjs tests/frontmatter.unit.test.cjs tests/core-utils.test.cjs tests/broken-windows.test.cjs';
|
||||
|
||||
/** @type {import('@stryker-mutator/core').PartialStrykerOptions} */
|
||||
export default {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"gsd-domain-researcher.md": 7032,
|
||||
"gsd-eval-auditor.md": 12496,
|
||||
"gsd-eval-planner.md": 7008,
|
||||
"gsd-executor.md": 46644,
|
||||
"gsd-executor.md": 47951,
|
||||
"gsd-framework-selector.md": 6778,
|
||||
"gsd-integration-checker.md": 15238,
|
||||
"gsd-intel-updater.md": 18166,
|
||||
|
||||
725
tests/broken-windows.test.cjs
Normal file
725
tests/broken-windows.test.cjs
Normal file
@@ -0,0 +1,725 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Broken-windows ledger — behavioral + property tests.
|
||||
*
|
||||
* Module: gsd-core/bin/lib/broken-windows.cjs (compiled from src/broken-windows.cts)
|
||||
* CLI: gsd-tools windows <status|append|waive|fixed>
|
||||
*
|
||||
* Issue: #1950 — enforced cross-phase defect register gating /gsd-ship.
|
||||
*
|
||||
* Coverage map (acceptance criteria from #1950):
|
||||
* - Executor writes stubs to ledger → append (CLI + pure)
|
||||
* - /gsd-ship fails while any entry is open → openCount + cmdWindowsStatus
|
||||
* - Waive requires non-empty reason → markWaived / cmdWindowsWaive
|
||||
* - Marking fixed removes from blocking set → markFixed / cmdWindowsMarkFixed
|
||||
* - Open-window count in progress surface → cmdWindowsStatus emits open_count
|
||||
* - Tests cover all four + clean-on-empty → empty ledger + full lifecycle
|
||||
*
|
||||
* Hermetic: each CLI test uses its own tmpdir via createTempDir and cleans up
|
||||
* via t.after() (CONTRIBUTING.md pattern 2). No shared state between tests.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const { createTempDir, cleanup, runGsdTools } = require('./helpers.cjs');
|
||||
const fc = require('./helpers/fast-check-setup.cjs');
|
||||
|
||||
const {
|
||||
REASON,
|
||||
WindowsError,
|
||||
LEDGER_FILE_NAME,
|
||||
emptyLedger,
|
||||
parseLedger,
|
||||
renderLedger,
|
||||
appendWindow,
|
||||
markWaived,
|
||||
markFixed,
|
||||
openCount,
|
||||
} = require('../gsd-core/bin/lib/broken-windows.cjs');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fixtures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Construct a minimal valid WindowEntry input for tests. */
|
||||
function makeEntry(overrides = {}) {
|
||||
return {
|
||||
kind: 'stub',
|
||||
phase: '2',
|
||||
description: 'hardcoded empty list',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/** Validator: matches a WindowsError carrying a specific REASON code. */
|
||||
function reasonIs(code) {
|
||||
return (err) => err instanceof WindowsError && err.reason === code;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure: emptyLedger + openCount
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('broken-windows: emptyLedger + openCount', () => {
|
||||
test('emptyLedger returns a ledger with zero counts and schema_version 1', () => {
|
||||
const led = emptyLedger('2026-07-19T00:00:00Z');
|
||||
assert.equal(led.schema_version, 1);
|
||||
assert.equal(led.open_count, 0);
|
||||
assert.equal(led.waived_count, 0);
|
||||
assert.equal(led.fixed_count, 0);
|
||||
assert.equal(led.total_count, 0);
|
||||
assert.equal(led.last_updated, '2026-07-19T00:00:00Z');
|
||||
assert.deepEqual(led.entries, []);
|
||||
});
|
||||
|
||||
test('openCount of empty ledger is 0 (clean-ship baseline)', () => {
|
||||
assert.equal(openCount(emptyLedger('now')), 0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure: appendWindow
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('broken-windows: appendWindow', () => {
|
||||
test('appending to an empty ledger assigns id=1, status=open, records timestamps', () => {
|
||||
const led0 = emptyLedger('2026-07-19T00:00:00Z');
|
||||
const { ledger, entry } = appendWindow(led0, makeEntry(), { now: '2026-07-19T12:00:00Z' });
|
||||
|
||||
assert.equal(entry.id, 1);
|
||||
assert.equal(entry.status, 'open');
|
||||
assert.equal(entry.recorded_at, '2026-07-19T12:00:00Z');
|
||||
assert.equal(entry.resolved_at, null);
|
||||
assert.equal(ledger.open_count, 1);
|
||||
assert.equal(ledger.total_count, 1);
|
||||
assert.equal(ledger.last_updated, '2026-07-19T12:00:00Z');
|
||||
});
|
||||
|
||||
test('second append gets id=2 (ids are dense and monotonic)', () => {
|
||||
let led = emptyLedger('now');
|
||||
({ ledger: led } = appendWindow(led, makeEntry({ description: 'first' }), { now: 't1' }));
|
||||
({ ledger: led } = appendWindow(led, makeEntry({ description: 'second' }), { now: 't2' }));
|
||||
assert.equal(led.entries[0].id, 1);
|
||||
assert.equal(led.entries[1].id, 2);
|
||||
assert.equal(led.total_count, 2);
|
||||
assert.equal(openCount(led), 2);
|
||||
});
|
||||
|
||||
test('append rejects unknown kind (fail-closed on schema drift)', () => {
|
||||
const led = emptyLedger('now');
|
||||
assert.throws(
|
||||
() => appendWindow(led, makeEntry({ kind: 'bogus' })),
|
||||
reasonIs(REASON.WINDOWS_INVALID_KIND),
|
||||
);
|
||||
});
|
||||
|
||||
test('append rejects empty description (no vacuous windows)', () => {
|
||||
const led = emptyLedger('now');
|
||||
assert.throws(
|
||||
() => appendWindow(led, makeEntry({ description: '' })),
|
||||
reasonIs(REASON.WINDOWS_APPEND_MISSING_FIELD),
|
||||
);
|
||||
assert.throws(
|
||||
() => appendWindow(led, makeEntry({ description: ' ' })),
|
||||
reasonIs(REASON.WINDOWS_APPEND_MISSING_FIELD),
|
||||
);
|
||||
});
|
||||
|
||||
test('append rejects path-traversal in --file (security boundary)', () => {
|
||||
const led = emptyLedger('now');
|
||||
assert.throws(
|
||||
() => appendWindow(led, makeEntry({ file: '../../etc/passwd' })),
|
||||
reasonIs(REASON.WINDOWS_INVALID_FILE),
|
||||
);
|
||||
});
|
||||
|
||||
test('append rejects 4-backtick run in description (H1 regression — would brick the JSON fence)', () => {
|
||||
const led = emptyLedger('now');
|
||||
assert.throws(
|
||||
() => appendWindow(led, makeEntry({ description: 'see ```` four backticks' })),
|
||||
reasonIs(REASON.WINDOWS_INVALID_TEXT),
|
||||
);
|
||||
// 3-backtick run is fine — the fence is 4-tick so 3-tick content is safe.
|
||||
const led2 = emptyLedger('now');
|
||||
const { ledger } = appendWindow(led2, makeEntry({ description: 'see ```js``` inline' }), { now: 't' });
|
||||
assert.equal(ledger.entries[0].description, 'see ```js``` inline');
|
||||
// And reparses cleanly:
|
||||
assert.doesNotThrow(() => parseLedger(renderLedger(ledger)));
|
||||
});
|
||||
|
||||
test('renderTable escapes backslash before pipe (CodeQL: incomplete-sanitization — PR #2441)', () => {
|
||||
// A description containing `\|` must NOT split the markdown table cell.
|
||||
// Escape order: `\` → `\\` first, then `|` → `\|`. If pipe is escaped first,
|
||||
// `\|` in input becomes `\\|` in output which markdown renders as `\` + cell-sep.
|
||||
const led0 = emptyLedger('2026-07-19T00:00:00Z');
|
||||
const { ledger } = appendWindow(
|
||||
led0,
|
||||
makeEntry({ description: 'path with \\| separator and | pipe and \\ backslash' }),
|
||||
{ now: '2026-07-19T12:00:00Z' },
|
||||
);
|
||||
const rendered = renderLedger(ledger);
|
||||
|
||||
// The JSON block (source of truth) preserves the description verbatim and reparses.
|
||||
const reparsed = parseLedger(rendered);
|
||||
assert.equal(reparsed.entries[0].description, 'path with \\| separator and | pipe and \\ backslash');
|
||||
|
||||
// The table row for this entry has exactly 10 cells (one per column). Counting
|
||||
// unescaped pipes inside the row would surface a split. The cell's rendered
|
||||
// form is `path with \\| separator and \| pipe and \\ backslash` — every pipe
|
||||
// is preceded by a backslash, so splitting on /(?<!\\)\|/ yields 10 cells.
|
||||
const tableLine = rendered.split('\n').find((l) => l.includes('path with'));
|
||||
assert.ok(tableLine, 'table row for the test entry must exist');
|
||||
// Walk the line and count pipes that are NOT preceded by a backslash.
|
||||
let unescapedPipes = 0;
|
||||
for (let i = 0; i < tableLine.length; i++) {
|
||||
if (tableLine[i] === '|' && tableLine[i - 1] !== '\\') unescapedPipes++;
|
||||
}
|
||||
// 10 cells = 11 cell-separator pipes per row (leading + 9 internal + trailing).
|
||||
assert.equal(unescapedPipes, 11, 'table row must have exactly 11 unescaped pipes (10 cells) — backslash-pipe in description must NOT add a split');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure: markWaived (acceptance: waive requires non-empty reason)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('broken-windows: markWaived', () => {
|
||||
test('waive with non-empty reason succeeds; waived_count increments; open_count decrements', () => {
|
||||
let led = emptyLedger('now');
|
||||
({ ledger: led } = appendWindow(led, makeEntry(), { now: 't1' }));
|
||||
led = markWaived(led, 1, 'Manual QA covers it', { now: 't2' });
|
||||
|
||||
assert.equal(led.entries[0].status, 'waived');
|
||||
assert.equal(led.entries[0].reason, 'Manual QA covers it');
|
||||
assert.equal(led.entries[0].resolved_at, 't2');
|
||||
assert.equal(led.open_count, 0);
|
||||
assert.equal(led.waived_count, 1);
|
||||
assert.equal(openCount(led), 0); // waived does not block
|
||||
});
|
||||
|
||||
test('waive with empty reason throws (boundary: limit-1 = 0 chars)', () => {
|
||||
let led = emptyLedger('now');
|
||||
({ ledger: led } = appendWindow(led, makeEntry(), { now: 't1' }));
|
||||
assert.throws(
|
||||
() => markWaived(led, 1, ''),
|
||||
reasonIs(REASON.WINDOWS_WAIVE_REASON_EMPTY),
|
||||
);
|
||||
});
|
||||
|
||||
test('waive with whitespace-only reason throws (boundary: limit = spaces)', () => {
|
||||
let led = emptyLedger('now');
|
||||
({ ledger: led } = appendWindow(led, makeEntry(), { now: 't1' }));
|
||||
assert.throws(
|
||||
() => markWaived(led, 1, ' '),
|
||||
reasonIs(REASON.WINDOWS_WAIVE_REASON_EMPTY),
|
||||
);
|
||||
});
|
||||
|
||||
test('waive with single-char reason succeeds (boundary: limit+1 = 1 char)', () => {
|
||||
let led = emptyLedger('now');
|
||||
({ ledger: led } = appendWindow(led, makeEntry(), { now: 't1' }));
|
||||
led = markWaived(led, 1, 'x', { now: 't2' });
|
||||
assert.equal(led.entries[0].status, 'waived');
|
||||
});
|
||||
|
||||
test('waive unknown id throws', () => {
|
||||
const led = emptyLedger('now');
|
||||
assert.throws(
|
||||
() => markWaived(led, 999, 'reason'),
|
||||
reasonIs(REASON.WINDOWS_ID_NOT_FOUND),
|
||||
);
|
||||
});
|
||||
|
||||
test('waive on already-resolved entry throws (no double-resolution)', () => {
|
||||
let led = emptyLedger('now');
|
||||
({ ledger: led } = appendWindow(led, makeEntry(), { now: 't1' }));
|
||||
led = markFixed(led, 1, { now: 't2' });
|
||||
assert.throws(
|
||||
() => markWaived(led, 1, 'late', { now: 't3' }),
|
||||
reasonIs(REASON.WINDOWS_ALREADY_RESOLVED),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure: markFixed (acceptance: fixed removes from blocking set)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('broken-windows: markFixed', () => {
|
||||
test('fixed decrements open_count and increments fixed_count', () => {
|
||||
let led = emptyLedger('now');
|
||||
({ ledger: led } = appendWindow(led, makeEntry(), { now: 't1' }));
|
||||
led = markFixed(led, 1, { now: 't2' });
|
||||
|
||||
assert.equal(led.entries[0].status, 'fixed');
|
||||
assert.equal(led.entries[0].resolved_at, 't2');
|
||||
assert.equal(led.open_count, 0);
|
||||
assert.equal(led.fixed_count, 1);
|
||||
assert.equal(openCount(led), 0);
|
||||
});
|
||||
|
||||
test('fixed on unknown id throws', () => {
|
||||
const led = emptyLedger('now');
|
||||
assert.throws(
|
||||
() => markFixed(led, 999),
|
||||
reasonIs(REASON.WINDOWS_ID_NOT_FOUND),
|
||||
);
|
||||
});
|
||||
|
||||
test('fixed on already-resolved throws', () => {
|
||||
let led = emptyLedger('now');
|
||||
({ ledger: led } = appendWindow(led, makeEntry(), { now: 't1' }));
|
||||
led = markWaived(led, 1, 'have it', { now: 't2' });
|
||||
assert.throws(
|
||||
() => markFixed(led, 1, { now: 't3' }),
|
||||
reasonIs(REASON.WINDOWS_ALREADY_RESOLVED),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure: parseLedger / renderLedger roundtrip (property test, fast-check)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('broken-windows: parse/render roundtrip property', () => {
|
||||
const arbKind = fc.constantFrom('stub', 'todo', 'fixme', 'skipped-test', 'lint-warning', 'unmet-truth', 'unrun-verify', 'deviation');
|
||||
const arbStatus = fc.constantFrom('open', 'waived', 'fixed');
|
||||
const arbPhase = fc.integer({ min: 1, max: 99 }).map(n => String(n));
|
||||
const arbText = fc.string({ minLength: 1, maxLength: 80 }).map(s => s.replace(/[\r\n\t|]/g, ' ').trim() || 'x');
|
||||
|
||||
const arbEntry = fc.record({
|
||||
id: fc.integer({ min: 1, max: 1000 }),
|
||||
kind: arbKind,
|
||||
phase: arbPhase,
|
||||
description: arbText,
|
||||
status: arbStatus,
|
||||
}).map((e) => ({
|
||||
id: e.id,
|
||||
kind: e.kind,
|
||||
phase: e.phase,
|
||||
file: e.id % 2 === 0 ? '' : `src/file${e.id}.ts`,
|
||||
line: e.id % 2 === 0 ? null : e.id * 10,
|
||||
description: e.description,
|
||||
status: e.status,
|
||||
reason: e.status === 'waived' ? 'justified' : '',
|
||||
recorded_at: '2026-07-19T00:00:00Z',
|
||||
resolved_at: e.status === 'open' ? null : '2026-07-19T01:00:00Z',
|
||||
}));
|
||||
|
||||
const arbLedger = fc.array(arbEntry, { maxLength: 6 }).map((entries) => {
|
||||
const open = entries.filter(e => e.status === 'open').length;
|
||||
const waived = entries.filter(e => e.status === 'waived').length;
|
||||
const fixed = entries.filter(e => e.status === 'fixed').length;
|
||||
return {
|
||||
schema_version: 1,
|
||||
open_count: open,
|
||||
waived_count: waived,
|
||||
fixed_count: fixed,
|
||||
total_count: entries.length,
|
||||
last_updated: '2026-07-19T00:00:00Z',
|
||||
entries,
|
||||
};
|
||||
});
|
||||
|
||||
test('property: render(parse(render(ledger))) === render(ledger)', () => {
|
||||
fc.assert(fc.property(arbLedger, (ledger) => {
|
||||
const rendered1 = renderLedger(ledger);
|
||||
const parsed = parseLedger(rendered1);
|
||||
const rendered2 = renderLedger(parsed);
|
||||
assert.equal(rendered2, rendered1, 'roundtrip must be stable');
|
||||
}));
|
||||
});
|
||||
|
||||
test('property: parseLedger never hangs or crashes on arbitrary unicode strings', () => {
|
||||
fc.assert(fc.property(fc.string({ maxLength: 200 }), (raw) => {
|
||||
try { parseLedger(raw); } catch { /* malformed input is allowed to throw */ }
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure: parseLedger fail-closed on malformed input
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('broken-windows: parseLedger fail-closed', () => {
|
||||
test('rejects frontmatter with wrong schema_version', () => {
|
||||
const raw = [
|
||||
'---',
|
||||
'schema_version: 99',
|
||||
'open_count: 0',
|
||||
'waived_count: 0',
|
||||
'fixed_count: 0',
|
||||
'total_count: 0',
|
||||
'last_updated: 2026-07-19T00:00:00Z',
|
||||
'---',
|
||||
'',
|
||||
'```json',
|
||||
'[]',
|
||||
'```',
|
||||
'',
|
||||
].join('\n');
|
||||
assert.throws(() => parseLedger(raw), reasonIs(REASON.WINDOWS_LEDGER_MALFORMED));
|
||||
});
|
||||
|
||||
test('rejects frontmatter missing open_count', () => {
|
||||
const raw = [
|
||||
'---',
|
||||
'schema_version: 1',
|
||||
'---',
|
||||
'',
|
||||
'```json',
|
||||
'[]',
|
||||
'```',
|
||||
'',
|
||||
].join('\n');
|
||||
assert.throws(() => parseLedger(raw), reasonIs(REASON.WINDOWS_LEDGER_MALFORMED));
|
||||
});
|
||||
|
||||
test('rejects frontmatter with non-numeric open_count', () => {
|
||||
const raw = [
|
||||
'---',
|
||||
'schema_version: 1',
|
||||
'open_count: "zero"',
|
||||
'---',
|
||||
'',
|
||||
'```json',
|
||||
'[]',
|
||||
'```',
|
||||
'',
|
||||
].join('\n');
|
||||
assert.throws(() => parseLedger(raw), reasonIs(REASON.WINDOWS_LEDGER_MALFORMED));
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI: gsd-tools windows status (acceptance: clean-ship on empty)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('broken-windows CLI: windows status', () => {
|
||||
test('status on a project with no ledger returns open_count=0 (backward-compat baseline)', (t) => {
|
||||
const tmp = createTempDir('bw-status-empty-');
|
||||
t.after(() => cleanup(tmp));
|
||||
|
||||
const res = runGsdTools(['windows', 'status', '--raw'], tmp);
|
||||
assert.equal(res.success, true, `stderr: ${res.error || ''}`);
|
||||
const obj = JSON.parse(res.output);
|
||||
assert.equal(obj.ok, true);
|
||||
assert.equal(obj.ledger.open_count, 0);
|
||||
assert.deepEqual(obj.ledger.entries, []);
|
||||
});
|
||||
|
||||
test('status on a malformed ledger fails closed', (t) => {
|
||||
const tmp = createTempDir('bw-status-malformed-');
|
||||
t.after(() => cleanup(tmp));
|
||||
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(tmp, '.planning', LEDGER_FILE_NAME),
|
||||
'not valid markdown or frontmatter',
|
||||
);
|
||||
|
||||
const res = runGsdTools(['windows', 'status', '--raw'], tmp);
|
||||
assert.equal(res.success, false);
|
||||
assert.ok(res.exitCode !== 0);
|
||||
assert.match(res.error, /malformed|invalid frontmatter|missing frontmatter/i);
|
||||
});
|
||||
|
||||
test('status on an UNREADABLE ledger fails closed (H2 regression — EACCES must not be silently empty)', (t) => {
|
||||
// Skip on Windows where chmod 000 doesn't apply to root/admin or where the FS
|
||||
// ignores mode bits; CI lanes run as non-root so the EACCES path is real.
|
||||
const tmp = createTempDir('bw-status-eacces-');
|
||||
t.after(() => {
|
||||
try { fs.chmodSync(path.join(tmp, '.planning', LEDGER_FILE_NAME), 0o644); } catch { /* best-effort */ }
|
||||
cleanup(tmp);
|
||||
});
|
||||
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
|
||||
// A ledger with open_count=1 — if EACCES silently returned empty, ship gate would pass.
|
||||
const validLedger = [
|
||||
'---',
|
||||
'schema_version: 1',
|
||||
'open_count: 1',
|
||||
'waived_count: 0',
|
||||
'fixed_count: 0',
|
||||
'total_count: 1',
|
||||
'last_updated: 2026-07-19T00:00:00Z',
|
||||
'---',
|
||||
'',
|
||||
'````json',
|
||||
JSON.stringify([{
|
||||
id: 1, kind: 'stub', phase: '2', file: '', line: null,
|
||||
description: 'unreadable-test', status: 'open', reason: '',
|
||||
recorded_at: 't', resolved_at: null,
|
||||
}]),
|
||||
'````',
|
||||
'',
|
||||
].join('\n');
|
||||
fs.writeFileSync(path.join(tmp, '.planning', LEDGER_FILE_NAME), validLedger);
|
||||
try { fs.chmodSync(path.join(tmp, '.planning', LEDGER_FILE_NAME), 0o000); } catch { return; }
|
||||
|
||||
const res = runGsdTools(['windows', 'status', '--raw'], tmp);
|
||||
// If the chmod actually took (non-root), the read must fail. If running as
|
||||
// root (CI rarely does), the read may succeed — either way, the test must
|
||||
// never see a false-green "open_count: 0" from a file we KNOW has open_count=1.
|
||||
if (res.success) {
|
||||
const obj = JSON.parse(res.output);
|
||||
assert.notEqual(obj.ledger.open_count, 0, 'EACCES must NOT silently coerce an open_count=1 ledger to 0');
|
||||
} else {
|
||||
assert.match(res.error, /could not read|EACCES|malformed/i);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI: gsd-tools windows append (acceptance: executor writes stubs)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('broken-windows CLI: windows append', () => {
|
||||
test('append creates the ledger if absent and records the entry', (t) => {
|
||||
const tmp = createTempDir('bw-append-create-');
|
||||
t.after(() => cleanup(tmp));
|
||||
|
||||
const res = runGsdTools(
|
||||
['windows', 'append', '--kind', 'stub', '--phase', '2',
|
||||
'--file', 'src/auth.ts', '--line', '42',
|
||||
'--description', 'hardcoded empty list in UserService.list'],
|
||||
tmp,
|
||||
);
|
||||
assert.equal(res.success, true, `stderr: ${res.error || ''}`);
|
||||
const obj = JSON.parse(res.output);
|
||||
assert.equal(obj.ok, true);
|
||||
assert.equal(obj.entry.id, 1);
|
||||
assert.equal(obj.entry.status, 'open');
|
||||
assert.equal(obj.ledger.open_count, 1);
|
||||
|
||||
// File exists with the right frontmatter and is re-readable.
|
||||
const ledgerPath = path.join(tmp, '.planning', LEDGER_FILE_NAME);
|
||||
assert.equal(fs.existsSync(ledgerPath), true);
|
||||
|
||||
// Second invocation observes the persisted entry (idempotent read).
|
||||
const res2 = runGsdTools(['windows', 'status', '--raw'], tmp);
|
||||
assert.equal(res2.success, true);
|
||||
const obj2 = JSON.parse(res2.output);
|
||||
assert.equal(obj2.ledger.open_count, 1);
|
||||
assert.equal(obj2.ledger.entries[0].id, 1);
|
||||
});
|
||||
|
||||
test('append a second entry gets id=2', (t) => {
|
||||
const tmp = createTempDir('bw-append-second-');
|
||||
t.after(() => cleanup(tmp));
|
||||
|
||||
const r1 = runGsdTools(
|
||||
['windows', 'append', '--kind', 'todo', '--phase', '2', '--description', 'first todo'],
|
||||
tmp,
|
||||
);
|
||||
assert.equal(r1.success, true, `stderr: ${r1.error || ''}`);
|
||||
const r2 = runGsdTools(
|
||||
['windows', 'append', '--kind', 'todo', '--phase', '2', '--description', 'second todo'],
|
||||
tmp,
|
||||
);
|
||||
assert.equal(r2.success, true);
|
||||
const obj2 = JSON.parse(r2.output);
|
||||
assert.equal(obj2.entry.id, 2);
|
||||
assert.equal(obj2.ledger.total_count, 2);
|
||||
});
|
||||
|
||||
test('append rejects unknown kind', (t) => {
|
||||
const tmp = createTempDir('bw-append-badkind-');
|
||||
t.after(() => cleanup(tmp));
|
||||
const res = runGsdTools(
|
||||
['windows', 'append', '--kind', 'bogus', '--phase', '2', '--description', 'x'],
|
||||
tmp,
|
||||
);
|
||||
assert.equal(res.success, false);
|
||||
assert.match(res.error, /invalid kind|allowed:/i);
|
||||
});
|
||||
|
||||
test('append rejects path-traversal in --file', (t) => {
|
||||
const tmp = createTempDir('bw-append-traversal-');
|
||||
t.after(() => cleanup(tmp));
|
||||
const res = runGsdTools(
|
||||
['windows', 'append', '--kind', 'stub', '--phase', '2',
|
||||
'--file', '../../etc/passwd', '--description', 'x'],
|
||||
tmp,
|
||||
);
|
||||
assert.equal(res.success, false);
|
||||
assert.match(res.error, /traversal|absolute|file/i);
|
||||
});
|
||||
|
||||
test('append rejects missing description', (t) => {
|
||||
const tmp = createTempDir('bw-append-nodesc-');
|
||||
t.after(() => cleanup(tmp));
|
||||
const res = runGsdTools(
|
||||
['windows', 'append', '--kind', 'stub', '--phase', '2'],
|
||||
tmp,
|
||||
);
|
||||
assert.equal(res.success, false);
|
||||
assert.match(res.error, /description|required|missing/i);
|
||||
});
|
||||
|
||||
test('append --line boundary: 0 / 1 / large int (limit-1 / limit / limit+1)', (t) => {
|
||||
const tmp = createTempDir('bw-append-line-bva-');
|
||||
t.after(() => cleanup(tmp));
|
||||
|
||||
// line=1: smallest valid line — limit boundary.
|
||||
const r1 = runGsdTools(['windows', 'append', '--kind', 'stub', '--phase', '2', '--line', '1', '--description', 'b'], tmp);
|
||||
assert.equal(r1.success, true, `--line 1 should succeed: ${r1.error || ''}`);
|
||||
assert.equal(JSON.parse(r1.output).entry.line, 1);
|
||||
|
||||
// line=large: limit+1 boundary (just confirm it accepts arbitrary positive int).
|
||||
const r2 = runGsdTools(['windows', 'append', '--kind', 'stub', '--phase', '2', '--line', '999999', '--description', 'c'], tmp);
|
||||
assert.equal(r2.success, true, `--line 999999 should succeed: ${r2.error || ''}`);
|
||||
assert.equal(JSON.parse(r2.output).entry.line, 999999);
|
||||
|
||||
// line=0: limit-1 boundary — invalid (lines are 1-indexed; 0 is not a line).
|
||||
// M2 fix: validateLine no longer treats 0 as omit; it rejects as non-positive.
|
||||
const rZero = runGsdTools(['windows', 'append', '--kind', 'stub', '--phase', '2', '--line', '0', '--description', 'a'], tmp);
|
||||
assert.equal(rZero.success, false, '--line 0 must fail (positive integers only)');
|
||||
assert.match(rZero.error, /line|positive integer/i);
|
||||
|
||||
// line=-1 and line=abc: also invalid — fail closed.
|
||||
const rNeg = runGsdTools(['windows', 'append', '--kind', 'stub', '--phase', '2', '--line', '-1', '--description', 'd'], tmp);
|
||||
assert.equal(rNeg.success, false);
|
||||
assert.match(rNeg.error, /line|positive integer/i);
|
||||
const rGarbage = runGsdTools(['windows', 'append', '--kind', 'stub', '--phase', '2', '--line', 'abc', '--description', 'e'], tmp);
|
||||
assert.equal(rGarbage.success, false);
|
||||
assert.match(rGarbage.error, /line|positive integer/i);
|
||||
|
||||
// line OMITTED entirely: valid, line is null.
|
||||
const rOmit = runGsdTools(['windows', 'append', '--kind', 'stub', '--phase', '2', '--description', 'f'], tmp);
|
||||
assert.equal(rOmit.success, true, `--line omitted should succeed: ${rOmit.error || ''}`);
|
||||
assert.equal(JSON.parse(rOmit.output).entry.line, null);
|
||||
});
|
||||
|
||||
test('append rejects 4-backtick description via CLI (H1 regression)', (t) => {
|
||||
const tmp = createTempDir('bw-append-4tick-');
|
||||
t.after(() => cleanup(tmp));
|
||||
const res = runGsdTools(
|
||||
['windows', 'append', '--kind', 'stub', '--phase', '2', '--description', 'has ```` four backticks'],
|
||||
tmp,
|
||||
);
|
||||
assert.equal(res.success, false);
|
||||
assert.match(res.error, /4-backtick|fence|invalid_text/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI: gsd-tools windows waive (acceptance: waive-with-reason)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('broken-windows CLI: windows waive', () => {
|
||||
test('waive with reason succeeds; subsequent status reports open_count=0', (t) => {
|
||||
const tmp = createTempDir('bw-waive-ok-');
|
||||
t.after(() => cleanup(tmp));
|
||||
|
||||
const r1 = runGsdTools(
|
||||
['windows', 'append', '--kind', 'skipped-test', '--phase', '3',
|
||||
'--file', 'tests/x.test.cjs', '--line', '18',
|
||||
'--description', 't.skip logout flow'],
|
||||
tmp,
|
||||
);
|
||||
assert.equal(r1.success, true, `stderr: ${r1.error || ''}`);
|
||||
|
||||
const r2 = runGsdTools(
|
||||
['windows', 'waive', '1', 'Manual QA covers it; CI cannot reach logout URL'],
|
||||
tmp,
|
||||
);
|
||||
assert.equal(r2.success, true, `stderr: ${r2.error || ''}`);
|
||||
const obj = JSON.parse(r2.output);
|
||||
assert.equal(obj.ok, true);
|
||||
assert.equal(obj.ledger.entries[0].status, 'waived');
|
||||
assert.equal(obj.ledger.entries[0].reason, 'Manual QA covers it; CI cannot reach logout URL');
|
||||
|
||||
const r3 = runGsdTools(['windows', 'status', '--raw'], tmp);
|
||||
assert.equal(r3.success, true);
|
||||
const status = JSON.parse(r3.output);
|
||||
assert.equal(status.ledger.open_count, 0); // waived does not block ship
|
||||
assert.equal(status.ledger.waived_count, 1);
|
||||
});
|
||||
|
||||
test('waive with empty reason fails', (t) => {
|
||||
const tmp = createTempDir('bw-waive-empty-');
|
||||
t.after(() => cleanup(tmp));
|
||||
const r1 = runGsdTools(
|
||||
['windows', 'append', '--kind', 'stub', '--phase', '2', '--description', 'x'],
|
||||
tmp,
|
||||
);
|
||||
assert.equal(r1.success, true, `stderr: ${r1.error || ''}`);
|
||||
|
||||
const r2 = runGsdTools(['windows', 'waive', '1', ''], tmp);
|
||||
assert.equal(r2.success, false);
|
||||
assert.match(r2.error, /waive.*reason|non-empty|reason.*required/i);
|
||||
});
|
||||
|
||||
test('waive unknown id fails', (t) => {
|
||||
const tmp = createTempDir('bw-waive-unknown-');
|
||||
t.after(() => cleanup(tmp));
|
||||
const res = runGsdTools(['windows', 'waive', '999', 'because'], tmp);
|
||||
assert.equal(res.success, false);
|
||||
assert.match(res.error, /no window|id 999|not found/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI: gsd-tools windows fixed (acceptance: fixed removes from blocking set)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('broken-windows CLI: windows fixed', () => {
|
||||
test('fixed removes the entry from the blocking set', (t) => {
|
||||
const tmp = createTempDir('bw-fixed-');
|
||||
t.after(() => cleanup(tmp));
|
||||
|
||||
const r1 = runGsdTools(
|
||||
['windows', 'append', '--kind', 'stub', '--phase', '2', '--description', 'x'],
|
||||
tmp,
|
||||
);
|
||||
assert.equal(r1.success, true, `stderr: ${r1.error || ''}`);
|
||||
|
||||
const rBefore = runGsdTools(['windows', 'status', '--raw'], tmp);
|
||||
assert.equal(rBefore.success, true);
|
||||
assert.equal(JSON.parse(rBefore.output).ledger.open_count, 1);
|
||||
|
||||
const r2 = runGsdTools(['windows', 'fixed', '1'], tmp);
|
||||
assert.equal(r2.success, true, `stderr: ${r2.error || ''}`);
|
||||
const obj = JSON.parse(r2.output);
|
||||
assert.equal(obj.ledger.open_count, 0);
|
||||
assert.equal(obj.ledger.fixed_count, 1);
|
||||
assert.equal(obj.ledger.entries[0].status, 'fixed');
|
||||
});
|
||||
|
||||
test('fixed on unknown id fails', (t) => {
|
||||
const tmp = createTempDir('bw-fixed-unknown-');
|
||||
t.after(() => cleanup(tmp));
|
||||
const res = runGsdTools(['windows', 'fixed', '999'], tmp);
|
||||
assert.equal(res.success, false);
|
||||
assert.match(res.error, /no window|id 999|not found/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI: full lifecycle — append → waive → append → fixed → clean ship
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('broken-windows CLI: lifecycle', () => {
|
||||
test('append two, waive one, fix one, then ship is clean', (t) => {
|
||||
const tmp = createTempDir('bw-lifecycle-');
|
||||
t.after(() => cleanup(tmp));
|
||||
|
||||
const r1 = runGsdTools(['windows', 'append', '--kind', 'stub', '--phase', '2', '--description', 'a'], tmp);
|
||||
const r2 = runGsdTools(['windows', 'append', '--kind', 'todo', '--phase', '2', '--description', 'b'], tmp);
|
||||
const r3 = runGsdTools(['windows', 'waive', '1', 'deferred to follow-up'], tmp);
|
||||
const r4 = runGsdTools(['windows', 'fixed', '2'], tmp);
|
||||
assert.equal(r1.success && r2.success && r3.success && r4.success, true,
|
||||
`lifecycle steps failed: r1=${r1.error || 'ok'} r2=${r2.error || 'ok'} r3=${r3.error || 'ok'} r4=${r4.error || 'ok'}`);
|
||||
|
||||
const rFinal = runGsdTools(['windows', 'status', '--raw'], tmp);
|
||||
assert.equal(rFinal.success, true);
|
||||
const status = JSON.parse(rFinal.output);
|
||||
assert.equal(status.ledger.open_count, 0); // ship gate would pass
|
||||
assert.equal(status.ledger.waived_count, 1);
|
||||
assert.equal(status.ledger.fixed_count, 1);
|
||||
assert.equal(status.ledger.total_count, 2);
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,7 @@
|
||||
"agents/gsd-domain-researcher.md": "1db46cac3f4d9889",
|
||||
"agents/gsd-eval-auditor.md": "1b8391f1aafb067f",
|
||||
"agents/gsd-eval-planner.md": "3d10fd11147f6857",
|
||||
"agents/gsd-executor.md": "152955064cf4c9d2",
|
||||
"agents/gsd-executor.md": "707f7f26bc2c1c59",
|
||||
"agents/gsd-framework-selector.md": "daa62c79619c76bf",
|
||||
"agents/gsd-integration-checker.md": "0643cd2d779b131c",
|
||||
"agents/gsd-intel-updater.md": "26c1f1e028c6346a",
|
||||
@@ -39,7 +39,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74",
|
||||
"gsd-core/bin/gsd-tools.cjs": "2fcf88250beb9214",
|
||||
"gsd-core/bin/gsd-tools.cjs": "8c5bf9788c3967be",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -289,7 +289,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "6cf726be6427a35a",
|
||||
"gsd-core/workflows/pr-branch.md": "dc5598ae8accdecd",
|
||||
"gsd-core/workflows/profile-user.md": "355af92ac285567f",
|
||||
"gsd-core/workflows/progress.md": "0e41db02a270a2b2",
|
||||
"gsd-core/workflows/progress.md": "9fb8c93970e0b4aa",
|
||||
"gsd-core/workflows/quick.md": "29739776f5c91338",
|
||||
"gsd-core/workflows/reapply-patches.md": "4dcd6117d0a507ca",
|
||||
"gsd-core/workflows/remove-phase.md": "23b9eb0858a2535e",
|
||||
@@ -302,7 +302,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "6d25100a9de15b31",
|
||||
"gsd-core/workflows/settings-integrations.md": "4d3001ad2b5dad8f",
|
||||
"gsd-core/workflows/settings.md": "8258f7bd3700d608",
|
||||
"gsd-core/workflows/ship.md": "8575dcc6d75a17c5",
|
||||
"gsd-core/workflows/ship.md": "3fb340ea87726ed2",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "0f842a609851a401",
|
||||
"gsd-core/workflows/sketch.md": "314b7d323c6b57eb",
|
||||
"gsd-core/workflows/smart-entry.md": "3ce5b6228238fdb6",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"agents/gsd-domain-researcher.md": "671c9ea949889c4a",
|
||||
"agents/gsd-eval-auditor.md": "fcaec7b00f94c435",
|
||||
"agents/gsd-eval-planner.md": "a4a5b4b3f7828ba3",
|
||||
"agents/gsd-executor.md": "47c648f6622e939f",
|
||||
"agents/gsd-executor.md": "28fbb5fa6eb49c65",
|
||||
"agents/gsd-framework-selector.md": "4b77eebbe9288d80",
|
||||
"agents/gsd-integration-checker.md": "fa53e2d78be1de74",
|
||||
"agents/gsd-intel-updater.md": "fa40e685d7441ace",
|
||||
@@ -110,7 +110,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
|
||||
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -360,7 +360,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
|
||||
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
||||
"gsd-core/workflows/profile-user.md": "14263db831230142",
|
||||
"gsd-core/workflows/progress.md": "9457fb10437c9402",
|
||||
"gsd-core/workflows/progress.md": "46c83ad61b3c4a84",
|
||||
"gsd-core/workflows/quick.md": "988fbe85a0251cf1",
|
||||
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||
@@ -373,7 +373,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44",
|
||||
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
|
||||
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
|
||||
"gsd-core/workflows/ship.md": "369f1461685d1897",
|
||||
"gsd-core/workflows/ship.md": "4642ebbaa38f8cc2",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "5f5ebb6a80d610c6",
|
||||
"gsd-core/workflows/sketch.md": "8319cedf3f93fc35",
|
||||
"gsd-core/workflows/smart-entry.md": "449238eb94abe187",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"agents/gsd-domain-researcher.md": "f1e03df842ddfb95",
|
||||
"agents/gsd-eval-auditor.md": "d0f45fff7370bb0b",
|
||||
"agents/gsd-eval-planner.md": "9cc049b82897daa4",
|
||||
"agents/gsd-executor.md": "3c8d4fd1c29542bb",
|
||||
"agents/gsd-executor.md": "a140ad7b13e4af95",
|
||||
"agents/gsd-framework-selector.md": "85005d716f9d98f7",
|
||||
"agents/gsd-integration-checker.md": "17a8ee731986564d",
|
||||
"agents/gsd-intel-updater.md": "4953a465db9dadc1",
|
||||
@@ -109,7 +109,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
|
||||
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -359,7 +359,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "fbe964fcdb244802",
|
||||
"gsd-core/workflows/pr-branch.md": "513f6cff722eff2d",
|
||||
"gsd-core/workflows/profile-user.md": "3b34dcb337d50f4b",
|
||||
"gsd-core/workflows/progress.md": "f6739fc007de6f66",
|
||||
"gsd-core/workflows/progress.md": "01079a85cbc41ffe",
|
||||
"gsd-core/workflows/quick.md": "0f49d4553de5da88",
|
||||
"gsd-core/workflows/reapply-patches.md": "44a96b52b975e9bb",
|
||||
"gsd-core/workflows/remove-phase.md": "8effc8742d58a11a",
|
||||
@@ -372,7 +372,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "94d61da368e9f85b",
|
||||
"gsd-core/workflows/settings-integrations.md": "dfe3672c4fabf139",
|
||||
"gsd-core/workflows/settings.md": "acdd79110699a608",
|
||||
"gsd-core/workflows/ship.md": "cae3fbefba655818",
|
||||
"gsd-core/workflows/ship.md": "30ff5e4ebf0edee8",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "d52a5462bafda830",
|
||||
"gsd-core/workflows/sketch.md": "dbe6acc4d976060c",
|
||||
"gsd-core/workflows/smart-entry.md": "1850447c045f36d8",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"agents/gsd-domain-researcher.md": "5f7d366251b957fe",
|
||||
"agents/gsd-eval-auditor.md": "fea2759beff0a642",
|
||||
"agents/gsd-eval-planner.md": "112f6730f23854e3",
|
||||
"agents/gsd-executor.md": "e63187a0bbff74eb",
|
||||
"agents/gsd-executor.md": "a3c5bd2940dcf7c6",
|
||||
"agents/gsd-framework-selector.md": "c350ee693cb1aa4e",
|
||||
"agents/gsd-integration-checker.md": "c8b4e65dee89c8ea",
|
||||
"agents/gsd-intel-updater.md": "5b41e05f90ce89d9",
|
||||
@@ -38,7 +38,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
|
||||
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -288,7 +288,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "af50e9f10d3cc6e1",
|
||||
"gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd",
|
||||
"gsd-core/workflows/profile-user.md": "ff3820a27731ceb8",
|
||||
"gsd-core/workflows/progress.md": "8aa2fcebb8b4876e",
|
||||
"gsd-core/workflows/progress.md": "1b56219777ea078c",
|
||||
"gsd-core/workflows/quick.md": "46da35933571f3f6",
|
||||
"gsd-core/workflows/reapply-patches.md": "ba9406b60f2c4041",
|
||||
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
|
||||
@@ -301,7 +301,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "339def28c34b0797",
|
||||
"gsd-core/workflows/settings-integrations.md": "53649313d20694ae",
|
||||
"gsd-core/workflows/settings.md": "7e7458cdb2b68ec5",
|
||||
"gsd-core/workflows/ship.md": "f777449b79ee3357",
|
||||
"gsd-core/workflows/ship.md": "cf9425876e736a52",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "121ed4b8127abf04",
|
||||
"gsd-core/workflows/sketch.md": "737c0492686fea2d",
|
||||
"gsd-core/workflows/smart-entry.md": "ad20cf74ae2e8291",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"agents/gsd-domain-researcher.md": "0fecdaea86466a56",
|
||||
"agents/gsd-eval-auditor.md": "36c44303085df2f8",
|
||||
"agents/gsd-eval-planner.md": "3ddea88a69b4da3f",
|
||||
"agents/gsd-executor.md": "343f7d59b160d74a",
|
||||
"agents/gsd-executor.md": "8443c52fc864d84e",
|
||||
"agents/gsd-framework-selector.md": "564669d479433f15",
|
||||
"agents/gsd-integration-checker.md": "1bbbdd3d420b994e",
|
||||
"agents/gsd-intel-updater.md": "42c40fffbc720d0b",
|
||||
@@ -42,7 +42,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "476aa24e8c4f03cf",
|
||||
"gsd-core/bin/gsd-tools.cjs": "81330ef8b3bb63c1",
|
||||
"gsd-core/bin/gsd-tools.cjs": "36d17f5566bdec9e",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -292,7 +292,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "a2cdd513663226f1",
|
||||
"gsd-core/workflows/pr-branch.md": "9923878a4f6a2d91",
|
||||
"gsd-core/workflows/profile-user.md": "26f74db0a7fcd268",
|
||||
"gsd-core/workflows/progress.md": "a2d948b084a48500",
|
||||
"gsd-core/workflows/progress.md": "53b4310b873d233e",
|
||||
"gsd-core/workflows/quick.md": "f4d73a7a52b11bd1",
|
||||
"gsd-core/workflows/reapply-patches.md": "eb4272145a117904",
|
||||
"gsd-core/workflows/remove-phase.md": "e336350f8113a328",
|
||||
@@ -305,7 +305,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "69f3a19bf2c61160",
|
||||
"gsd-core/workflows/settings-integrations.md": "76eee76d6eb57657",
|
||||
"gsd-core/workflows/settings.md": "3701faed09d55247",
|
||||
"gsd-core/workflows/ship.md": "a58d05ab03124d0b",
|
||||
"gsd-core/workflows/ship.md": "e2eb0a000c515798",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "1f44789553180d84",
|
||||
"gsd-core/workflows/sketch.md": "2226779b6003a71c",
|
||||
"gsd-core/workflows/smart-entry.md": "9a64f43927641ca4",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"agents/gsd-domain-researcher.md": "1c1a800108a2b225",
|
||||
"agents/gsd-eval-auditor.md": "99012004b14ea602",
|
||||
"agents/gsd-eval-planner.md": "4ebdd7fe9cbb0cfe",
|
||||
"agents/gsd-executor.md": "44149e405a126f90",
|
||||
"agents/gsd-executor.md": "87ae9188c389bde8",
|
||||
"agents/gsd-framework-selector.md": "7726fccc86bfeb50",
|
||||
"agents/gsd-integration-checker.md": "2d8339790bbb2dc3",
|
||||
"agents/gsd-intel-updater.md": "c51339956197cbd3",
|
||||
@@ -110,7 +110,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
|
||||
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -360,7 +360,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
|
||||
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
||||
"gsd-core/workflows/profile-user.md": "4fa910d15dea5695",
|
||||
"gsd-core/workflows/progress.md": "9457fb10437c9402",
|
||||
"gsd-core/workflows/progress.md": "46c83ad61b3c4a84",
|
||||
"gsd-core/workflows/quick.md": "68a60c8fe332cb4e",
|
||||
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||
@@ -373,7 +373,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44",
|
||||
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
|
||||
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
|
||||
"gsd-core/workflows/ship.md": "369f1461685d1897",
|
||||
"gsd-core/workflows/ship.md": "4642ebbaa38f8cc2",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "2aba89ecd8f41a0d",
|
||||
"gsd-core/workflows/sketch.md": "5eedd93f9a5b49d5",
|
||||
"gsd-core/workflows/smart-entry.md": "449238eb94abe187",
|
||||
|
||||
10
tests/fixtures/golden-install-parity/codex.json
vendored
10
tests/fixtures/golden-install-parity/codex.json
vendored
@@ -102,8 +102,8 @@
|
||||
"agents/gsd-eval-auditor.toml": "9b81d61b3c5f722d",
|
||||
"agents/gsd-eval-planner.md": "73f2ad2ff2797a51",
|
||||
"agents/gsd-eval-planner.toml": "09468ad1a34ac468",
|
||||
"agents/gsd-executor.md": "ed8080096522f7ec",
|
||||
"agents/gsd-executor.toml": "126d79d90e2fdf4f",
|
||||
"agents/gsd-executor.md": "45442cc002be7052",
|
||||
"agents/gsd-executor.toml": "bfb08e9397bcc358",
|
||||
"agents/gsd-framework-selector.md": "ebae32430887d2e0",
|
||||
"agents/gsd-framework-selector.toml": "637e4e021b7ec380",
|
||||
"agents/gsd-integration-checker.md": "9cc875676cf7d741",
|
||||
@@ -145,7 +145,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
|
||||
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -395,7 +395,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "5b07de07e4593281",
|
||||
"gsd-core/workflows/pr-branch.md": "d13e1cc81de40896",
|
||||
"gsd-core/workflows/profile-user.md": "05828c8cc61ef384",
|
||||
"gsd-core/workflows/progress.md": "03f12b223eb3a96e",
|
||||
"gsd-core/workflows/progress.md": "e57d5a148df38508",
|
||||
"gsd-core/workflows/quick.md": "ef0af6ee7788f64b",
|
||||
"gsd-core/workflows/reapply-patches.md": "26297b84736e66a4",
|
||||
"gsd-core/workflows/remove-phase.md": "9ee0fddd11a0d9d4",
|
||||
@@ -408,7 +408,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "2431433811616f76",
|
||||
"gsd-core/workflows/settings-integrations.md": "77730321d3d6d317",
|
||||
"gsd-core/workflows/settings.md": "054c8c31b3905ced",
|
||||
"gsd-core/workflows/ship.md": "2c2ab4e0243a2b27",
|
||||
"gsd-core/workflows/ship.md": "ccd8cda30a4a4231",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "07724a390fbb43f6",
|
||||
"gsd-core/workflows/sketch.md": "576f300dfdde1b7d",
|
||||
"gsd-core/workflows/smart-entry.md": "6f686195ae531b03",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"agents/gsd-domain-researcher.agent.md": "d603239b3e9fe428",
|
||||
"agents/gsd-eval-auditor.agent.md": "3c03009564de55c8",
|
||||
"agents/gsd-eval-planner.agent.md": "14751876fc2b5f16",
|
||||
"agents/gsd-executor.agent.md": "e75e46a35b7b260a",
|
||||
"agents/gsd-executor.agent.md": "bca7d1ab12392956",
|
||||
"agents/gsd-framework-selector.agent.md": "cafeec0b3489be45",
|
||||
"agents/gsd-integration-checker.agent.md": "30439b804927acc7",
|
||||
"agents/gsd-intel-updater.agent.md": "238c1a886f35a25c",
|
||||
@@ -40,7 +40,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74",
|
||||
"gsd-core/bin/gsd-tools.cjs": "2fcf88250beb9214",
|
||||
"gsd-core/bin/gsd-tools.cjs": "8c5bf9788c3967be",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -290,7 +290,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "21e461cd39e5181b",
|
||||
"gsd-core/workflows/pr-branch.md": "2833905f119b5722",
|
||||
"gsd-core/workflows/profile-user.md": "5cc032206c99ef71",
|
||||
"gsd-core/workflows/progress.md": "449b7f4de1bef7ae",
|
||||
"gsd-core/workflows/progress.md": "97f4971366c381ee",
|
||||
"gsd-core/workflows/quick.md": "dff6d086ef16a14f",
|
||||
"gsd-core/workflows/reapply-patches.md": "8fd59e24b486f180",
|
||||
"gsd-core/workflows/remove-phase.md": "e262654e319d1bc4",
|
||||
@@ -303,7 +303,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "230a658de9c017a6",
|
||||
"gsd-core/workflows/settings-integrations.md": "a1d146d6bfd14db5",
|
||||
"gsd-core/workflows/settings.md": "f611f14f2f447f1e",
|
||||
"gsd-core/workflows/ship.md": "7e52f4b11bd15b58",
|
||||
"gsd-core/workflows/ship.md": "9a260c559e783769",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "f2590cb6ddbfad94",
|
||||
"gsd-core/workflows/sketch.md": "e2063966439af8c1",
|
||||
"gsd-core/workflows/smart-entry.md": "6c707b959a41900b",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"agents/gsd-domain-researcher.md": "56395dbdabf076f6",
|
||||
"agents/gsd-eval-auditor.md": "ad2840fd5cd76172",
|
||||
"agents/gsd-eval-planner.md": "2049dac060d00eda",
|
||||
"agents/gsd-executor.md": "e3d78836e115b18c",
|
||||
"agents/gsd-executor.md": "e568f8171c7d12f1",
|
||||
"agents/gsd-framework-selector.md": "4b77eebbe9288d80",
|
||||
"agents/gsd-integration-checker.md": "5da30584d06b878c",
|
||||
"agents/gsd-intel-updater.md": "b8971c5d96e63b38",
|
||||
@@ -110,7 +110,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "2525f1ae8b086828",
|
||||
"gsd-core/bin/gsd-tools.cjs": "31f45cfd36e5dfdb",
|
||||
"gsd-core/bin/gsd-tools.cjs": "40e8e83311b87500",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -360,7 +360,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "7bccd151ce7b69f2",
|
||||
"gsd-core/workflows/pr-branch.md": "c67d90c65da47168",
|
||||
"gsd-core/workflows/profile-user.md": "8c943983241260b5",
|
||||
"gsd-core/workflows/progress.md": "0f03aaf96f5abf7e",
|
||||
"gsd-core/workflows/progress.md": "2dbc495deed1a36d",
|
||||
"gsd-core/workflows/quick.md": "ad40466dd04e4147",
|
||||
"gsd-core/workflows/reapply-patches.md": "ba9406b60f2c4041",
|
||||
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
|
||||
@@ -373,7 +373,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "bf8ea69c8f7ae019",
|
||||
"gsd-core/workflows/settings-integrations.md": "166ca51b1f33c2a3",
|
||||
"gsd-core/workflows/settings.md": "0623c673eaf04799",
|
||||
"gsd-core/workflows/ship.md": "6e3ac35dc933a39a",
|
||||
"gsd-core/workflows/ship.md": "1515bb4909387b1d",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "5be73b7bdf96b539",
|
||||
"gsd-core/workflows/sketch.md": "373bc0d83368a411",
|
||||
"gsd-core/workflows/smart-entry.md": "c8fc316358cdcd7b",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"agents/gsd-domain-researcher.md": "412cdbb05ba252ea",
|
||||
"agents/gsd-eval-auditor.md": "4ffb265063c318e5",
|
||||
"agents/gsd-eval-planner.md": "03448fc9c5774b56",
|
||||
"agents/gsd-executor.md": "b70c976372c77dc1",
|
||||
"agents/gsd-executor.md": "e39b2cd4ec6b3afd",
|
||||
"agents/gsd-framework-selector.md": "ea9981d65d6b3429",
|
||||
"agents/gsd-integration-checker.md": "35b4f2969d279871",
|
||||
"agents/gsd-intel-updater.md": "5fe5edfae2719cb8",
|
||||
@@ -39,7 +39,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "3a3409215044af9f",
|
||||
"gsd-core/bin/gsd-tools.cjs": "6df1804284c68471",
|
||||
"gsd-core/bin/gsd-tools.cjs": "c7e11161f0296129",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -289,7 +289,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "f862f77fca983749",
|
||||
"gsd-core/workflows/pr-branch.md": "ecabd55e4eabf229",
|
||||
"gsd-core/workflows/profile-user.md": "de5030437226cf2c",
|
||||
"gsd-core/workflows/progress.md": "debe980226d9260d",
|
||||
"gsd-core/workflows/progress.md": "589f60eb809c0bb9",
|
||||
"gsd-core/workflows/quick.md": "fab58ce8581251fa",
|
||||
"gsd-core/workflows/reapply-patches.md": "158083a310859594",
|
||||
"gsd-core/workflows/remove-phase.md": "fce799aae3ab2715",
|
||||
@@ -302,7 +302,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "49be159144d7f426",
|
||||
"gsd-core/workflows/settings-integrations.md": "1dce76db0aca08a5",
|
||||
"gsd-core/workflows/settings.md": "9ced580679ed0255",
|
||||
"gsd-core/workflows/ship.md": "8e8c1d3c8204769d",
|
||||
"gsd-core/workflows/ship.md": "b2f16c18a7e1e19b",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "f1ece50ac65ea281",
|
||||
"gsd-core/workflows/sketch.md": "d5887983e62b574a",
|
||||
"gsd-core/workflows/smart-entry.md": "47f5c5e8608e5f7a",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"agents/gsd-domain-researcher.md": "a3874d80bcbc7380",
|
||||
"agents/gsd-eval-auditor.md": "630d4cd3bd6ea195",
|
||||
"agents/gsd-eval-planner.md": "3db12cde12aeb2c1",
|
||||
"agents/gsd-executor.md": "73278ea652cac62f",
|
||||
"agents/gsd-executor.md": "14c1681bb5f7214f",
|
||||
"agents/gsd-framework-selector.md": "ad5f2c6b9bec6270",
|
||||
"agents/gsd-integration-checker.md": "c503e2f4a3d8ec05",
|
||||
"agents/gsd-intel-updater.md": "231393da62a45b2e",
|
||||
@@ -110,7 +110,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
|
||||
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -360,7 +360,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "ffa5774304243649",
|
||||
"gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd",
|
||||
"gsd-core/workflows/profile-user.md": "203ebe3f8f3876a8",
|
||||
"gsd-core/workflows/progress.md": "c2a8a79af6603e70",
|
||||
"gsd-core/workflows/progress.md": "172e4168e326ca61",
|
||||
"gsd-core/workflows/quick.md": "4ef7da71a596604a",
|
||||
"gsd-core/workflows/reapply-patches.md": "becf9728cdb124c4",
|
||||
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
|
||||
@@ -373,7 +373,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "edd858cd6cfddaf1",
|
||||
"gsd-core/workflows/settings-integrations.md": "3ea8095d5fad891b",
|
||||
"gsd-core/workflows/settings.md": "1925ecc2225c2216",
|
||||
"gsd-core/workflows/ship.md": "7afe19fa2d472013",
|
||||
"gsd-core/workflows/ship.md": "cb5f2f25ca80bd3b",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "888c0548e63197b3",
|
||||
"gsd-core/workflows/sketch.md": "34a0c10fa56af7ea",
|
||||
"gsd-core/workflows/smart-entry.md": "7ffe4fdb93935400",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"agents/subagents/gsd-eval-auditor.yaml": "e3d868bd5fefe938",
|
||||
"agents/subagents/gsd-eval-planner.md": "70f8c5727bfb9876",
|
||||
"agents/subagents/gsd-eval-planner.yaml": "df8499f7af297ec2",
|
||||
"agents/subagents/gsd-executor.md": "20cf0b2aa886d35c",
|
||||
"agents/subagents/gsd-executor.md": "e37a910127d9ca5c",
|
||||
"agents/subagents/gsd-executor.yaml": "e29422986636fd64",
|
||||
"agents/subagents/gsd-framework-selector.md": "a15b7aa1e0576e16",
|
||||
"agents/subagents/gsd-framework-selector.yaml": "fb52c31cde27b0e3",
|
||||
@@ -103,7 +103,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
|
||||
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -353,7 +353,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
|
||||
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
||||
"gsd-core/workflows/profile-user.md": "5abfae83739fa978",
|
||||
"gsd-core/workflows/progress.md": "9457fb10437c9402",
|
||||
"gsd-core/workflows/progress.md": "46c83ad61b3c4a84",
|
||||
"gsd-core/workflows/quick.md": "2a9476454b69b4ff",
|
||||
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||
@@ -366,7 +366,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44",
|
||||
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
|
||||
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
|
||||
"gsd-core/workflows/ship.md": "369f1461685d1897",
|
||||
"gsd-core/workflows/ship.md": "4642ebbaa38f8cc2",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "b767a1d3db129a8a",
|
||||
"gsd-core/workflows/sketch.md": "88cfdf4edcf222ab",
|
||||
"gsd-core/workflows/smart-entry.md": "449238eb94abe187",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"agents/gsd-domain-researcher.md": "71250e759ca9e723",
|
||||
"agents/gsd-eval-auditor.md": "c88890105f32ace6",
|
||||
"agents/gsd-eval-planner.md": "60bddb70a937f796",
|
||||
"agents/gsd-executor.md": "84c0a7dc19ddb6c5",
|
||||
"agents/gsd-executor.md": "4946be74860ea68e",
|
||||
"agents/gsd-framework-selector.md": "1c0a10355e787675",
|
||||
"agents/gsd-integration-checker.md": "a9de5928e5a5c649",
|
||||
"agents/gsd-intel-updater.md": "493e07482fa6198a",
|
||||
@@ -110,7 +110,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
|
||||
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -360,7 +360,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "b8dad652e31c2318",
|
||||
"gsd-core/workflows/pr-branch.md": "929b7cb0c99c7b9e",
|
||||
"gsd-core/workflows/profile-user.md": "248d59a31948e0ed",
|
||||
"gsd-core/workflows/progress.md": "352920a61ed4f4db",
|
||||
"gsd-core/workflows/progress.md": "c3d7149133d571a9",
|
||||
"gsd-core/workflows/quick.md": "f2641bad95c6614e",
|
||||
"gsd-core/workflows/reapply-patches.md": "a0e9b53f90abceb2",
|
||||
"gsd-core/workflows/remove-phase.md": "dea4661e8f89596f",
|
||||
@@ -373,7 +373,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "252b0d3edc315339",
|
||||
"gsd-core/workflows/settings-integrations.md": "d1711a95f44fdbde",
|
||||
"gsd-core/workflows/settings.md": "2e42ee34c791378a",
|
||||
"gsd-core/workflows/ship.md": "fb70add4f61cf3e8",
|
||||
"gsd-core/workflows/ship.md": "9e773a94f4abf0e6",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "681800323681c5c6",
|
||||
"gsd-core/workflows/sketch.md": "fcb7af914159ef7b",
|
||||
"gsd-core/workflows/smart-entry.md": "2a253fe437496eea",
|
||||
|
||||
6
tests/fixtures/golden-install-parity/pi.json
vendored
6
tests/fixtures/golden-install-parity/pi.json
vendored
@@ -6,7 +6,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
|
||||
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -256,7 +256,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
|
||||
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
||||
"gsd-core/workflows/profile-user.md": "1bac7f69142801ef",
|
||||
"gsd-core/workflows/progress.md": "9457fb10437c9402",
|
||||
"gsd-core/workflows/progress.md": "46c83ad61b3c4a84",
|
||||
"gsd-core/workflows/quick.md": "c00a6574ff650a86",
|
||||
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||
@@ -269,7 +269,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44",
|
||||
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
|
||||
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
|
||||
"gsd-core/workflows/ship.md": "369f1461685d1897",
|
||||
"gsd-core/workflows/ship.md": "4642ebbaa38f8cc2",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "838c701bd072ae73",
|
||||
"gsd-core/workflows/sketch.md": "c7725562b3efd311",
|
||||
"gsd-core/workflows/smart-entry.md": "449238eb94abe187",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"agents/gsd-domain-researcher.md": "bd054bb27beed2a7",
|
||||
"agents/gsd-eval-auditor.md": "57cc7458ab5de6b7",
|
||||
"agents/gsd-eval-planner.md": "01b665728dde4ccf",
|
||||
"agents/gsd-executor.md": "6dcccfef842a49ac",
|
||||
"agents/gsd-executor.md": "a6dd5b7da329d8f2",
|
||||
"agents/gsd-framework-selector.md": "82ba6abea84226b7",
|
||||
"agents/gsd-integration-checker.md": "90835dbc7dfa1691",
|
||||
"agents/gsd-intel-updater.md": "3cc4f6ddd04676ec",
|
||||
@@ -39,7 +39,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "6e98d76e955e35a2",
|
||||
"gsd-core/bin/gsd-tools.cjs": "ebbae1afe8c7fcd5",
|
||||
"gsd-core/bin/gsd-tools.cjs": "c67b43694423939a",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -289,7 +289,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "0a92ba12993ac261",
|
||||
"gsd-core/workflows/pr-branch.md": "cef0f65b16d500b4",
|
||||
"gsd-core/workflows/profile-user.md": "263c0693563d98da",
|
||||
"gsd-core/workflows/progress.md": "4ceb0f6f5e34c4a4",
|
||||
"gsd-core/workflows/progress.md": "475eb60a2e739eed",
|
||||
"gsd-core/workflows/quick.md": "f5de70665fbea4fa",
|
||||
"gsd-core/workflows/reapply-patches.md": "de0ee8acfe7245b2",
|
||||
"gsd-core/workflows/remove-phase.md": "e8ae4fbbfac700f0",
|
||||
@@ -302,7 +302,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "5e05212fb5cba531",
|
||||
"gsd-core/workflows/settings-integrations.md": "29c5de27fbbb18e9",
|
||||
"gsd-core/workflows/settings.md": "26b9b7979d3a5747",
|
||||
"gsd-core/workflows/ship.md": "55cc3a0b0d188021",
|
||||
"gsd-core/workflows/ship.md": "15420ac2a46fc203",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "89e0eab2af946b04",
|
||||
"gsd-core/workflows/sketch.md": "483387542d6fc3af",
|
||||
"gsd-core/workflows/smart-entry.md": "d309710bcabd4675",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"agents/gsd-domain-researcher.md": "b80f76874c04e515",
|
||||
"agents/gsd-eval-auditor.md": "470bf16303ec4d2e",
|
||||
"agents/gsd-eval-planner.md": "22334fde85723c9d",
|
||||
"agents/gsd-executor.md": "b3baa767abac0593",
|
||||
"agents/gsd-executor.md": "a611d34bf51e6aec",
|
||||
"agents/gsd-framework-selector.md": "7726fccc86bfeb50",
|
||||
"agents/gsd-integration-checker.md": "7cd2072984411c7f",
|
||||
"agents/gsd-intel-updater.md": "83de6ba9172891c3",
|
||||
@@ -39,7 +39,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "de4627dff103d527",
|
||||
"gsd-core/bin/gsd-tools.cjs": "1673458b3332be8a",
|
||||
"gsd-core/bin/gsd-tools.cjs": "44e114ddf0f32e08",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -289,7 +289,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "856ad565b2eb0c47",
|
||||
"gsd-core/workflows/pr-branch.md": "79fd55b88ea2db9c",
|
||||
"gsd-core/workflows/profile-user.md": "672821e6b1266645",
|
||||
"gsd-core/workflows/progress.md": "00de1afbe907efa5",
|
||||
"gsd-core/workflows/progress.md": "1fe098b4d559ba55",
|
||||
"gsd-core/workflows/quick.md": "95ad5630aca7f1e2",
|
||||
"gsd-core/workflows/reapply-patches.md": "21b38c374f19fd78",
|
||||
"gsd-core/workflows/remove-phase.md": "a46c2fe853bf4e86",
|
||||
@@ -302,7 +302,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "39e66386f6c48025",
|
||||
"gsd-core/workflows/settings-integrations.md": "f8f756709ec02363",
|
||||
"gsd-core/workflows/settings.md": "44b10c59215633b8",
|
||||
"gsd-core/workflows/ship.md": "7d6e81541d393db2",
|
||||
"gsd-core/workflows/ship.md": "58d9fd3ee13ce1c2",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "dbec602d104cb951",
|
||||
"gsd-core/workflows/sketch.md": "44ff275150b6d045",
|
||||
"gsd-core/workflows/smart-entry.md": "d8018578571f08d5",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"agents/gsd-domain-researcher.md": "56395dbdabf076f6",
|
||||
"agents/gsd-eval-auditor.md": "fb64fc5acf359747",
|
||||
"agents/gsd-eval-planner.md": "2049dac060d00eda",
|
||||
"agents/gsd-executor.md": "45c4632bc67a12a2",
|
||||
"agents/gsd-executor.md": "067a4f846f4e020c",
|
||||
"agents/gsd-framework-selector.md": "4b77eebbe9288d80",
|
||||
"agents/gsd-integration-checker.md": "4ffb37fb230c2b90",
|
||||
"agents/gsd-intel-updater.md": "a81d77c143c02108",
|
||||
@@ -39,7 +39,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "5636ca0b726871b2",
|
||||
"gsd-core/bin/gsd-tools.cjs": "ecc544c0f939432b",
|
||||
"gsd-core/bin/gsd-tools.cjs": "3224906065bfb20c",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -289,7 +289,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "6cf61f540cdbd8b9",
|
||||
"gsd-core/workflows/pr-branch.md": "acd59f915d018ad4",
|
||||
"gsd-core/workflows/profile-user.md": "c4313672b81b5bcd",
|
||||
"gsd-core/workflows/progress.md": "7b6b9564b34fcabb",
|
||||
"gsd-core/workflows/progress.md": "3e4ac518362ba665",
|
||||
"gsd-core/workflows/quick.md": "baff92603ade7489",
|
||||
"gsd-core/workflows/reapply-patches.md": "d449a23d3acf6379",
|
||||
"gsd-core/workflows/remove-phase.md": "e7a6af429b36e77b",
|
||||
@@ -302,7 +302,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "2f86ec7b998f9485",
|
||||
"gsd-core/workflows/settings-integrations.md": "b082fc518b484c07",
|
||||
"gsd-core/workflows/settings.md": "002eb0ce3c10c741",
|
||||
"gsd-core/workflows/ship.md": "6f611c07a1d34ff2",
|
||||
"gsd-core/workflows/ship.md": "63ea5e85cbd33058",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "10063f56c2c7f141",
|
||||
"gsd-core/workflows/sketch.md": "25c1f8f7acfb1da9",
|
||||
"gsd-core/workflows/smart-entry.md": "bd81482cb6a7ac53",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"agents/gsd-domain-researcher.md": "049f588663814fa2",
|
||||
"agents/gsd-eval-auditor.md": "54870d3b07433525",
|
||||
"agents/gsd-eval-planner.md": "552e9fa164c51ce8",
|
||||
"agents/gsd-executor.md": "59fbb1e047b9a7c4",
|
||||
"agents/gsd-executor.md": "1154cdd7b22f9754",
|
||||
"agents/gsd-framework-selector.md": "8a795f230436ad2e",
|
||||
"agents/gsd-integration-checker.md": "c1760a0bbd4f7bf5",
|
||||
"agents/gsd-intel-updater.md": "944f1d903e2e9e09",
|
||||
@@ -110,7 +110,7 @@
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||
"gsd-core/bin/gsd-tools.cjs": "acf82db0f1b06ae8",
|
||||
"gsd-core/bin/gsd-tools.cjs": "d119a8ff93c9c0a3",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
@@ -360,7 +360,7 @@
|
||||
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
|
||||
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
||||
"gsd-core/workflows/profile-user.md": "e23bea0a69c0bb4b",
|
||||
"gsd-core/workflows/progress.md": "9457fb10437c9402",
|
||||
"gsd-core/workflows/progress.md": "46c83ad61b3c4a84",
|
||||
"gsd-core/workflows/quick.md": "5331f238c75a821f",
|
||||
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||
@@ -373,7 +373,7 @@
|
||||
"gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44",
|
||||
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
|
||||
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
|
||||
"gsd-core/workflows/ship.md": "369f1461685d1897",
|
||||
"gsd-core/workflows/ship.md": "4642ebbaa38f8cc2",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "86db87b16548117e",
|
||||
"gsd-core/workflows/sketch.md": "e96f1866d3e60cab",
|
||||
"gsd-core/workflows/smart-entry.md": "449238eb94abe187",
|
||||
|
||||
@@ -291,19 +291,27 @@ describe('frontmatter get SECURITY.md threats_open — type contract', () => {
|
||||
|
||||
describe('real registry ship:pre — structural guards', () => {
|
||||
|
||||
test('ship:pre byLoopPoint entry has exactly 1 gate and 0 steps/contributions', () => {
|
||||
test('ship:pre byLoopPoint entry has only gates (no steps/contributions) and includes security + broken-windows', () => {
|
||||
const entry = realRegistry.byLoopPoint['ship:pre'];
|
||||
assert.ok(entry, 'ship:pre must exist in byLoopPoint');
|
||||
assert.strictEqual(entry.steps.length, 0, 'ship:pre must have 0 steps');
|
||||
assert.strictEqual(entry.contributions.length, 0, 'ship:pre must have 0 contributions');
|
||||
assert.strictEqual(entry.gates.length, 1, 'ship:pre must have exactly 1 gate (security)');
|
||||
// Two predicate-style gates as of #1950: security (workflow.security_enforcement)
|
||||
// and broken-windows (workflow.windows_enforce). Both default-off in tests via
|
||||
// their respective when keys; both surface in the registry regardless of activation.
|
||||
assert.strictEqual(entry.gates.length, 2, 'ship:pre must have exactly 2 gates (security + broken-windows)');
|
||||
const capIds = entry.gates.map(g => g.capId).sort();
|
||||
assert.deepEqual(capIds, ['broken-windows', 'security'], 'ship:pre gate capIds must be {security, broken-windows}');
|
||||
});
|
||||
|
||||
test('ship:pre gate capId is "security" and check has predicate not query', () => {
|
||||
const gate = realRegistry.byLoopPoint['ship:pre'].gates[0];
|
||||
assert.strictEqual(gate.capId, 'security');
|
||||
assert.ok(gate.check.predicate, 'ship:pre gate must use predicate, not query');
|
||||
assert.strictEqual(gate.check.query, undefined, 'ship:pre must NOT have a check.query (predicate-only gate)');
|
||||
test('every ship:pre gate uses predicate (not query) and the security gate is present', () => {
|
||||
const gates = realRegistry.byLoopPoint['ship:pre'].gates;
|
||||
for (const gate of gates) {
|
||||
assert.ok(gate.check.predicate, `ship:pre gate ${gate.capId} must use predicate, not query`);
|
||||
assert.strictEqual(gate.check.query, undefined, `ship:pre gate ${gate.capId} must NOT have a check.query (predicate-only gate)`);
|
||||
}
|
||||
const security = gates.find(g => g.capId === 'security');
|
||||
assert.ok(security, 'ship:pre must include the security gate');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"plant-seed.md": 11785,
|
||||
"pr-branch.md": 15963,
|
||||
"profile-user.md": 21246,
|
||||
"progress.md": 31789,
|
||||
"progress.md": 32632,
|
||||
"quick.md": 50993,
|
||||
"reapply-patches.md": 20312,
|
||||
"remove-phase.md": 8513,
|
||||
@@ -71,7 +71,7 @@
|
||||
"settings-advanced.md": 40019,
|
||||
"settings-integrations.md": 15892,
|
||||
"settings.md": 33467,
|
||||
"ship.md": 25575,
|
||||
"ship.md": 27945,
|
||||
"sketch-wrap-up.md": 14267,
|
||||
"sketch.md": 20004,
|
||||
"smart-entry.md": 11124,
|
||||
|
||||
Reference in New Issue
Block a user