@opengsd/gsd-core 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +131 -34
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-executor.md +7 -5
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-plan-checker.md +9 -0
- package/agents/gsd-planner.md +5 -8
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-verifier.md +14 -70
- package/bin/install.js +503 -341
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +607 -63
- package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
- package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
- package/gsd-core/bin/lib/api-coverage.cjs +120 -0
- package/gsd-core/bin/lib/audit.cjs +89 -1
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-registry.cjs +96 -110
- package/gsd-core/bin/lib/capability-validator.cjs +12 -2
- package/gsd-core/bin/lib/check-command-router.cjs +43 -1
- package/gsd-core/bin/lib/command-aliases.cjs +72 -0
- package/gsd-core/bin/lib/commands.cjs +26 -25
- package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +12 -1
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +91 -12
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/frontmatter.cjs +84 -12
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1021 -57
- package/gsd-core/bin/lib/install-engine.cjs +64 -10
- package/gsd-core/bin/lib/install-profiles.cjs +27 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
- package/gsd-core/bin/lib/io.cjs +28 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone.cjs +106 -51
- package/gsd-core/bin/lib/phase-id.cjs +63 -0
- package/gsd-core/bin/lib/phase-locator.cjs +138 -45
- package/gsd-core/bin/lib/phase.cjs +260 -25
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
- package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
- package/gsd-core/bin/lib/roadmap.cjs +37 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
- package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
- package/gsd-core/bin/lib/smart-entry.cjs +12 -0
- package/gsd-core/bin/lib/state-transition.cjs +73 -8
- package/gsd-core/bin/lib/state.cjs +151 -62
- package/gsd-core/bin/lib/surface.cjs +12 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
- package/gsd-core/bin/lib/uat.cjs +320 -21
- package/gsd-core/bin/lib/unusable-input.cjs +9 -0
- package/gsd-core/bin/lib/verification.cjs +29 -12
- package/gsd-core/bin/lib/verify.cjs +29 -5
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
- package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
- package/gsd-core/bin/lib/workstream.cjs +6 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/specless-probe-fallback.md +7 -6
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +32 -69
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
- package/gsd-core/workflows/code-review.md +42 -145
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +23 -81
- package/gsd-core/workflows/debug.md +9 -12
- package/gsd-core/workflows/diagnose-issues.md +22 -0
- package/gsd-core/workflows/discovery-phase.md +4 -4
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +65 -137
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/ingest-docs.md +2 -1
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +21 -38
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +13 -226
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +49 -193
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +11 -153
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +20 -390
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +15 -8
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +17 -14
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +49 -11
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +8 -21
- package/gsd-core/workflows/ui-phase.md +8 -7
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +18 -7
- package/gsd-core/workflows/verify-phase.md +4 -7
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +8 -58
- package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
- package/hooks/dist/gsd-check-update-worker.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
- package/hooks/dist/gsd-read-injection-scanner.js +7 -0
- package/hooks/dist/gsd-statusline.js +72 -6
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/isolation-sentinel.js +268 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +428 -0
- package/hooks/gsd-check-update-worker.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +532 -26
- package/hooks/gsd-read-injection-scanner.js +7 -0
- package/hooks/gsd-statusline.js +72 -6
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/isolation-sentinel.js +268 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +14 -5
- package/pi/gsd.cjs +57 -12
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +9 -2
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-context-index.cjs +448 -0
- package/scripts/gen-inventory-manifest.cjs +101 -1
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-test-file-count.allowlist.json +27 -1
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/prompt-injection-scan.sh +27 -6
- package/scripts/registry-schema.cjs +323 -94
- package/scripts/run-tests.cjs +3 -2
- package/scripts/validate-registry.cjs +10 -6
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +2 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-emitted-baseline.cjs +0 -145
|
@@ -21,6 +21,13 @@
|
|
|
21
21
|
* here would be a MODULE_NOT_FOUND in the published package. The duplication is bounded
|
|
22
22
|
* by a parity test — `tests/emitted-attribution.test.cjs` runs both surfaces over one
|
|
23
23
|
* corpus and fails if they ever disagree about what is schema-valid.
|
|
24
|
+
*
|
|
25
|
+
* #2914: the single shared `tests/emitted-drift-ack.json` is replaced by per-PR
|
|
26
|
+
* fragments under `tests/emitted-drift-acks/` (kept alongside the legacy file, which is
|
|
27
|
+
* still honored). This validator now checks BOTH: every physical source is run through
|
|
28
|
+
* the same schema/policy rules below, and — because two sources are never allowed to
|
|
29
|
+
* name the same path (silent last-wins would resurrect exactly the silent-drift class
|
|
30
|
+
* the ack seam exists to end) — a cross-source duplicate key is ALSO a hard failure.
|
|
24
31
|
*/
|
|
25
32
|
|
|
26
33
|
const fs = require('node:fs');
|
|
@@ -28,10 +35,36 @@ const path = require('node:path');
|
|
|
28
35
|
|
|
29
36
|
const ACK_VERSION = 1;
|
|
30
37
|
const ACK_REPO_PATH = 'tests/emitted-drift-ack.json';
|
|
38
|
+
const ACK_DIR_REPO_PATH = 'tests/emitted-drift-acks';
|
|
31
39
|
const REPO_ROOT = path.join(__dirname, '..');
|
|
32
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Upper bound on how many fragment files `listFragmentFiles` may return in one
|
|
43
|
+
* `readdirSync` pass. Mirrors `MAX_ACK_FRAGMENTS` in `tests/helpers/emitted-diff.cjs` —
|
|
44
|
+
* DUPLICATED rather than imported, because `scripts/` ships in the npm package and
|
|
45
|
+
* `tests/` does not (requiring across that line would be MODULE_NOT_FOUND once
|
|
46
|
+
* published; see this file's top-of-file comment). The two are held to the same value
|
|
47
|
+
* by the schema-parity test in `tests/emitted-attribution.test.cjs`.
|
|
48
|
+
*
|
|
49
|
+
* Exceeding it throws rather than truncating: a truncated listing would silently drop
|
|
50
|
+
* acknowledgments from consideration, which is exactly the class of silent failure this
|
|
51
|
+
* whole ack seam exists to prevent.
|
|
52
|
+
*/
|
|
53
|
+
const MAX_ACK_FRAGMENTS = 500;
|
|
54
|
+
|
|
33
55
|
const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
34
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Key names that can never be a legitimate emitted path or bare workflow/agent filename
|
|
59
|
+
* (`__proto__`, `constructor`, `prototype`). Duplicated (not imported) in
|
|
60
|
+
* `tests/helpers/emitted-diff.cjs`'s `parseAck` for the same reason every other constant
|
|
61
|
+
* here is duplicated rather than required — `scripts/` ships, `tests/` does not. Held to
|
|
62
|
+
* the same set by the schema-parity test in `tests/emitted-attribution.test.cjs`, which
|
|
63
|
+
* must see BOTH surfaces reject a document naming one of these, never one silently
|
|
64
|
+
* accepting what the other errors on (#2914 review).
|
|
65
|
+
*/
|
|
66
|
+
const RESERVED_ACK_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
67
|
+
|
|
35
68
|
/**
|
|
36
69
|
* Validate an ack document's raw text.
|
|
37
70
|
*
|
|
@@ -42,9 +75,13 @@ const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArr
|
|
|
42
75
|
* rather than left behind.
|
|
43
76
|
*
|
|
44
77
|
* @param {string|null} raw file contents, or null when the file is absent
|
|
78
|
+
* @param {object} [opts]
|
|
79
|
+
* @param {string} [opts.source] the path used in error messages (default: the legacy
|
|
80
|
+
* file). Generalized (#2914) so the same rules apply verbatim to a fragment under
|
|
81
|
+
* `ACK_DIR_REPO_PATH` — one definition of "valid", named per the file it is checking.
|
|
45
82
|
* @returns {{ schemaErrors: string[], policyErrors: string[], ok: boolean }}
|
|
46
83
|
*/
|
|
47
|
-
function validateAckText(raw) {
|
|
84
|
+
function validateAckText(raw, { source = ACK_REPO_PATH } = {}) {
|
|
48
85
|
const schemaErrors = [];
|
|
49
86
|
const policyErrors = [];
|
|
50
87
|
const done = () => ({ schemaErrors, policyErrors, ok: schemaErrors.length === 0 && policyErrors.length === 0 });
|
|
@@ -52,7 +89,7 @@ function validateAckText(raw) {
|
|
|
52
89
|
if (raw === null) return done(); // absent is the healthy steady state
|
|
53
90
|
|
|
54
91
|
if (raw.trim() === '') {
|
|
55
|
-
schemaErrors.push(`${
|
|
92
|
+
schemaErrors.push(`${source} is present but empty`);
|
|
56
93
|
return done();
|
|
57
94
|
}
|
|
58
95
|
|
|
@@ -60,7 +97,7 @@ function validateAckText(raw) {
|
|
|
60
97
|
try {
|
|
61
98
|
doc = JSON.parse(raw);
|
|
62
99
|
} catch (err) {
|
|
63
|
-
schemaErrors.push(`${
|
|
100
|
+
schemaErrors.push(`${source} is not valid JSON: ${err.message}`);
|
|
64
101
|
return done();
|
|
65
102
|
}
|
|
66
103
|
|
|
@@ -70,7 +107,7 @@ function validateAckText(raw) {
|
|
|
70
107
|
// it declares nothing, so the remedy is the same as an entryless document.
|
|
71
108
|
if (doc === null) {
|
|
72
109
|
policyErrors.push(
|
|
73
|
-
`${
|
|
110
|
+
`${source} contains "null" and declares no acknowledgments. Delete the file — `
|
|
74
111
|
+ 'the healthy steady state is no file at all.',
|
|
75
112
|
);
|
|
76
113
|
return done();
|
|
@@ -78,28 +115,41 @@ function validateAckText(raw) {
|
|
|
78
115
|
|
|
79
116
|
if (!isPlainObject(doc)) {
|
|
80
117
|
schemaErrors.push(
|
|
81
|
-
`${
|
|
118
|
+
`${source}: must be a JSON object, got ${Array.isArray(doc) ? 'array' : typeof doc}`,
|
|
82
119
|
);
|
|
83
120
|
return done();
|
|
84
121
|
}
|
|
85
122
|
|
|
86
123
|
if (doc.version !== undefined && doc.version !== ACK_VERSION) {
|
|
87
124
|
schemaErrors.push(
|
|
88
|
-
`${
|
|
125
|
+
`${source}: unsupported version ${JSON.stringify(doc.version)} (expected ${ACK_VERSION})`,
|
|
89
126
|
);
|
|
90
127
|
}
|
|
91
128
|
|
|
92
129
|
const paths = doc.paths;
|
|
93
130
|
if (paths !== undefined && !isPlainObject(paths)) {
|
|
94
|
-
schemaErrors.push(`${
|
|
131
|
+
schemaErrors.push(`${source}: "paths" must be an object of <emitted path> -> { reason }`);
|
|
95
132
|
return done();
|
|
96
133
|
}
|
|
97
134
|
|
|
98
135
|
const entries = paths === undefined ? [] : Object.entries(paths);
|
|
99
136
|
for (const [rel, value] of entries) {
|
|
137
|
+
if (RESERVED_ACK_KEYS.has(rel)) {
|
|
138
|
+
// Reject loudly rather than silently filter. Previously this key was excluded
|
|
139
|
+
// only from `declaredKeys`'s duplicate-detection view, so a document naming it
|
|
140
|
+
// passed validation here while the gate's `parseAck` (fed the JSON.parse'd
|
|
141
|
+
// document, where such a key is a genuine own property) either mishandled it or
|
|
142
|
+
// disagreed silently — two surfaces reaching different verdicts on the same
|
|
143
|
+
// document (#2914 review). Recognizably the same finding as `parseAck`'s.
|
|
144
|
+
schemaErrors.push(
|
|
145
|
+
`${source}: ack key "${rel}" is reserved and can never be a valid emitted path `
|
|
146
|
+
+ 'or workflow/agent filename — remove it',
|
|
147
|
+
);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
100
150
|
const reason = isPlainObject(value) ? value.reason : value;
|
|
101
151
|
if (typeof reason !== 'string' || reason.trim() === '') {
|
|
102
|
-
schemaErrors.push(`${
|
|
152
|
+
schemaErrors.push(`${source}: ack for "${rel}" has no non-empty "reason"`);
|
|
103
153
|
}
|
|
104
154
|
}
|
|
105
155
|
|
|
@@ -108,7 +158,7 @@ function validateAckText(raw) {
|
|
|
108
158
|
// behind after removing the last entry by hand.
|
|
109
159
|
if (entries.length === 0) {
|
|
110
160
|
policyErrors.push(
|
|
111
|
-
`${
|
|
161
|
+
`${source} is present but declares no acknowledgments. Delete the file — an `
|
|
112
162
|
+ 'empty one signals nothing, and the healthy steady state is no file at all.',
|
|
113
163
|
);
|
|
114
164
|
}
|
|
@@ -120,30 +170,175 @@ function readIfPresent(file) {
|
|
|
120
170
|
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
|
|
121
171
|
}
|
|
122
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Fragment filenames under `dir`, sorted. Absent directory == zero fragments.
|
|
175
|
+
*
|
|
176
|
+
* Fails loudly, naming `dir`, the cap, and the actual count, when the directory holds
|
|
177
|
+
* more than `MAX_ACK_FRAGMENTS` entries — never silently truncates the listing.
|
|
178
|
+
*/
|
|
179
|
+
function listFragmentFiles(dir) {
|
|
180
|
+
if (!fs.existsSync(dir)) return [];
|
|
181
|
+
const names = fs.readdirSync(dir).filter((name) => name.endsWith('.json')).sort();
|
|
182
|
+
if (names.length > MAX_ACK_FRAGMENTS) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`lint-emitted-drift-ack: ${dir} contains ${names.length} ack fragments, exceeding `
|
|
185
|
+
+ `the cap of ${MAX_ACK_FRAGMENTS}. Refusing to read only some of them — a truncated `
|
|
186
|
+
+ 'read would silently drop acknowledgments. Prune spent fragments from this directory.',
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return names;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The path keys a document declares, for cross-source collision detection — but ONLY
|
|
194
|
+
* when the document is itself trustworthy. A document that failed its own schema check
|
|
195
|
+
* must not also seed a bogus "collision" derived from garbage; its own error already
|
|
196
|
+
* blocks the merge, and reporting a fabricated collision on top would confuse rather
|
|
197
|
+
* than clarify. `RESERVED_ACK_KEYS` are also excluded here — they can never be a
|
|
198
|
+
* legitimate duplicate, since they can never be a legitimate key at all — but this is
|
|
199
|
+
* belt-and-suspenders, not the enforcement point: `validateAckText` above now rejects any
|
|
200
|
+
* document naming one outright, so `main()` only ever calls this on a document whose
|
|
201
|
+
* schema already checked out, making the exclusion below unreachable in practice.
|
|
202
|
+
*/
|
|
203
|
+
function declaredKeys(raw) {
|
|
204
|
+
if (raw === null) return [];
|
|
205
|
+
let doc;
|
|
206
|
+
try {
|
|
207
|
+
doc = JSON.parse(raw);
|
|
208
|
+
} catch {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
if (!isPlainObject(doc)) return [];
|
|
212
|
+
const paths = doc.paths;
|
|
213
|
+
if (paths === undefined) return [];
|
|
214
|
+
if (!isPlainObject(paths)) return [];
|
|
215
|
+
return Object.keys(paths).filter((k) => !RESERVED_ACK_KEYS.has(k));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* assertAbsentOnNext — the `next`-lane guard (#2914), invoked only by the
|
|
220
|
+
* `guard-no-ack-on-next` workflow job on push to `next`, never in `lint:ci`.
|
|
221
|
+
*
|
|
222
|
+
* `validateAckText` lints SHAPE, because a PR's own working tree may legitimately carry
|
|
223
|
+
* a live, well-formed ack — that is the normal case a PR-lane check must allow. This
|
|
224
|
+
* function instead rejects PRESENCE outright, valid or not: per the ack-lifecycle law
|
|
225
|
+
* (#2789, `RULESET.EMITTED_ATTRIBUTION`), an entry already at the base is spent the
|
|
226
|
+
* moment it merges, so a document surviving on `next` is inert cruft by definition, not
|
|
227
|
+
* a thing to schema-check.
|
|
228
|
+
*
|
|
229
|
+
* This MUST NOT run as a PR-lane check comparing a PR against `next` — that is the #2768
|
|
230
|
+
* shape #2789 exists to prevent (a spent-but-present base ack would red every open PR the
|
|
231
|
+
* instant one landed). It is safe only because it runs on `next` itself, asserting a fact
|
|
232
|
+
* about `next`'s own tree, never about any PR's diff against it.
|
|
233
|
+
*
|
|
234
|
+
* @param {boolean} present whether ACK_REPO_PATH exists in the tree being checked
|
|
235
|
+
* @returns {{ ok: boolean, message: string }}
|
|
236
|
+
*/
|
|
237
|
+
function assertAbsentOnNext(present) {
|
|
238
|
+
if (!present) {
|
|
239
|
+
return { ok: true, message: `ok guard-no-ack-on-next: ${ACK_REPO_PATH} is absent (the healthy steady state)` };
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
message: [
|
|
244
|
+
`guard-no-ack-on-next: ${ACK_REPO_PATH} exists on next.`,
|
|
245
|
+
'',
|
|
246
|
+
'Every entry in this file is scoped to the diff that introduced it (#2789). Once merged '
|
|
247
|
+
+ 'to next it is, by definition, already at the base -- spent and inert, regardless of '
|
|
248
|
+
+ 'whether it is otherwise well-formed.',
|
|
249
|
+
'',
|
|
250
|
+
'#2914: acks now go in per-PR fragments under tests/emitted-drift-acks/, one file per '
|
|
251
|
+
+ 'PR, never this single shared file -- a persistent fragment there is harmless (every '
|
|
252
|
+
+ 'fragment is independently named, so it cannot conflict with any other PR), which is '
|
|
253
|
+
+ 'why only THIS legacy file is guarded here, never the fragment directory.',
|
|
254
|
+
'',
|
|
255
|
+
'CONTRIBUTING.md: "When you remove the last entry from tests/emitted-drift-ack.json, '
|
|
256
|
+
+ 'delete the file too -- its presence is the alarm."',
|
|
257
|
+
'',
|
|
258
|
+
`Remedy: git rm ${ACK_REPO_PATH}`,
|
|
259
|
+
].join('\n'),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
123
263
|
function main() {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
264
|
+
const legacyFile = path.join(REPO_ROOT, ...ACK_REPO_PATH.split('/'));
|
|
265
|
+
|
|
266
|
+
if (process.argv.includes('--guard-next')) {
|
|
267
|
+
const result = assertAbsentOnNext(fs.existsSync(legacyFile));
|
|
268
|
+
console.log(result.message);
|
|
269
|
+
if (!result.ok) process.exitCode = 1;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const fragmentsDir = path.join(REPO_ROOT, ...ACK_DIR_REPO_PATH.split('/'));
|
|
274
|
+
const sources = [
|
|
275
|
+
{ label: ACK_REPO_PATH, raw: readIfPresent(legacyFile) },
|
|
276
|
+
...listFragmentFiles(fragmentsDir).map((name) => ({
|
|
277
|
+
label: `${ACK_DIR_REPO_PATH}/${name}`,
|
|
278
|
+
raw: readIfPresent(path.join(fragmentsDir, name)),
|
|
279
|
+
})),
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
const problems = [];
|
|
283
|
+
const owner = new Map(); // path key -> the source label that already claimed it
|
|
284
|
+
let anyPresent = false;
|
|
285
|
+
|
|
286
|
+
for (const { label, raw } of sources) {
|
|
287
|
+
if (raw !== null) anyPresent = true;
|
|
288
|
+
|
|
289
|
+
// `validateAckText` already prefixes every message with `source` (== `label`), so
|
|
290
|
+
// these are pushed verbatim rather than re-prefixed — a second prefix would read as
|
|
291
|
+
// "tests/emitted-drift-acks/x.json: tests/emitted-drift-acks/x.json is not valid
|
|
292
|
+
// JSON", naming the same file twice for no reason.
|
|
293
|
+
const result = validateAckText(raw, { source: label });
|
|
294
|
+
problems.push(...result.schemaErrors, ...result.policyErrors);
|
|
295
|
+
|
|
296
|
+
// Only chase collisions across documents whose OWN schema already checked out —
|
|
297
|
+
// a document we could not trust must not also seed a fabricated collision.
|
|
298
|
+
if (result.schemaErrors.length === 0) {
|
|
299
|
+
for (const key of declaredKeys(raw)) {
|
|
300
|
+
if (owner.has(key)) {
|
|
301
|
+
problems.push(
|
|
302
|
+
`duplicate ack for "${key}": declared in both ${owner.get(key)} and ${label}. `
|
|
303
|
+
+ 'Two ack sources (fragments, or a fragment and the legacy file) may never '
|
|
304
|
+
+ 'name the same path — rename or merge them.',
|
|
305
|
+
);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
owner.set(key, label);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
127
312
|
|
|
128
|
-
if (
|
|
129
|
-
console.error(`lint-emitted-drift-ack: ${
|
|
130
|
-
for (const e of
|
|
313
|
+
if (problems.length) {
|
|
314
|
+
console.error(`lint-emitted-drift-ack: ${problems.length} problem(s)\n`);
|
|
315
|
+
for (const e of problems) console.error(` - ${e}`);
|
|
131
316
|
console.error(
|
|
132
317
|
'\nThis blocks the merge on purpose. The base-side reader fails loudly on a document '
|
|
133
318
|
+ 'it cannot parse, so a broken one on the base branch reds every PR that carries an '
|
|
134
|
-
+ 'acknowledgment
|
|
319
|
+
+ 'acknowledgment, and a duplicate across two sources is exactly the silent-drift class '
|
|
320
|
+
+ 'the ack seam exists to end. Fix or delete the offending source(s) here, where it is cheap.',
|
|
135
321
|
);
|
|
136
322
|
process.exitCode = 1;
|
|
137
323
|
return;
|
|
138
324
|
}
|
|
139
325
|
|
|
140
326
|
console.log(
|
|
141
|
-
|
|
142
|
-
?
|
|
143
|
-
:
|
|
327
|
+
anyPresent
|
|
328
|
+
? 'ok lint-emitted-drift-ack: all acknowledgment sources are well-formed'
|
|
329
|
+
: 'ok lint-emitted-drift-ack: no acknowledgment sources present (the healthy steady state)',
|
|
144
330
|
);
|
|
145
331
|
}
|
|
146
332
|
|
|
147
333
|
if (require.main === module) main();
|
|
148
334
|
|
|
149
|
-
module.exports = {
|
|
335
|
+
module.exports = {
|
|
336
|
+
validateAckText,
|
|
337
|
+
assertAbsentOnNext,
|
|
338
|
+
declaredKeys,
|
|
339
|
+
listFragmentFiles,
|
|
340
|
+
ACK_VERSION,
|
|
341
|
+
ACK_REPO_PATH,
|
|
342
|
+
ACK_DIR_REPO_PATH,
|
|
343
|
+
MAX_ACK_FRAGMENTS,
|
|
344
|
+
};
|