nccgs 2.0.0 → 2.1.1
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/agents/nccgs-adversarial-reviewer.md +1 -1
- package/.claude/agents/nccgs-engine-programmer.md +1 -1
- package/.claude/agents/nccgs-fast-implementer.md +1 -1
- package/.claude/agents/nccgs-game-designer.md +1 -1
- package/.claude/agents/nccgs-gameplay-programmer.md +1 -1
- package/.claude/agents/nccgs-qa-engineer.md +1 -1
- package/.claude/agents/nccgs-task-scout.md +1 -1
- package/.claude/agents/nccgs-ui-programmer.md +1 -1
- package/.claude/agents/nccgs-unity-build-specialist.md +1 -1
- package/.claude/agents/nccgs-unity-implementer.md +1 -1
- package/.claude/agents/nccgs-unity-systems-specialist.md +1 -1
- package/.claude/agents/nccgs-unity-ui-specialist.md +1 -1
- package/.claude/agents/nccgs-verification-engineer.md +1 -1
- package/.claude/nccgs/MEMORY.md +9 -3
- package/.claude/nccgs/VERSION +1 -1
- package/.claude/nccgs/constitution.md +187 -176
- package/.claude/nccgs/hooks/closure-guard.mjs +2 -0
- package/.claude/nccgs/hooks/unity-transport-guard.mjs +11 -0
- package/.claude/nccgs/pipeline.json +260 -260
- package/.claude/nccgs/protocols/context-packets.md +5 -0
- package/.claude/nccgs/protocols/model-routing.md +46 -46
- package/.claude/nccgs/protocols/review-handoff.md +21 -1
- package/.claude/nccgs/protocols/shared-workflow-context.md +33 -0
- package/.claude/nccgs/protocols/unity-boundary.md +20 -0
- package/.claude/nccgs/routing.generated.md +4 -4
- package/.claude/nccgs/settings.fragment.json +84 -24
- package/.claude/nccgs/studio.json +53 -53
- package/.claude/nccgs/tools/autonomy.mjs +13 -0
- package/.claude/nccgs/tools/command-tokens.mjs +22 -0
- package/.claude/nccgs/tools/compile-policy.mjs +4 -0
- package/.claude/nccgs/tools/configure-models.mjs +4 -0
- package/.claude/nccgs/tools/hook-transport.mjs +34 -0
- package/.claude/nccgs/tools/installation-guard.mjs +55 -0
- package/.claude/nccgs/tools/native-completion.mjs +10 -1
- package/.claude/nccgs/tools/returned-handoff.mjs +11 -2
- package/.claude/nccgs/tools/shared-context-guard.mjs +60 -0
- package/.claude/nccgs/tools/shared-context.mjs +25 -0
- package/.claude/nccgs/tools/tool-guard.mjs +120 -131
- package/.claude/nccgs/tools/unity-transport.mjs +170 -0
- package/.claude/nccgs/workflow-catalog.json +4 -4
- package/CHANGELOG.md +235 -165
- package/CLAUDE.md +10 -8
- package/MEMORY.md +109 -56
- package/README.md +206 -168
- package/UPGRADING.md +130 -112
- package/VERSION +1 -1
- package/docs/ARCHITECTURE.md +131 -131
- package/docs/ASSET-PRODUCTION-GRAPHIFY-PLAN.md +230 -0
- package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +117 -87
- package/docs/NCCGS-1.5.md +237 -237
- package/docs/PROJECT-POLICY.md +94 -94
- package/docs/RELEASE-STATUS.md +32 -7
- package/docs/UNINSTALL.md +134 -0
- package/docs/WFK17-HOOK-REPAIR.md +71 -0
- package/docs/WORKFLOWS.md +85 -85
- package/package.json +1 -1
- package/scaffold/.nccgs/project.yaml +109 -108
- package/scripts/benchmark-v15.mjs +173 -173
- package/scripts/cli.mjs +11 -2
- package/scripts/doctor.mjs +261 -261
- package/scripts/install.mjs +530 -515
- package/scripts/recover-session.mjs +89 -0
- package/scripts/run.mjs +22 -9
- package/scripts/supervise.mjs +4 -0
- package/scripts/uninstall.mjs +292 -0
- package/scripts/validate.mjs +285 -278
- package/tests/autonomy.test.mjs +126 -113
- package/tests/design-readiness.test.mjs +199 -199
- package/tests/fixtures/compiler-shutdown.cs +67 -64
- package/tests/fixtures/native-provider.mjs +61 -0
- package/tests/framework.test.mjs +740 -740
- package/tests/hook-repair.test.mjs +106 -0
- package/tests/lifecycle-v15.test.mjs +346 -300
- package/tests/native-completion-notices.test.mjs +181 -0
- package/tests/phase1.test.mjs +427 -416
- package/tests/phase2.test.mjs +250 -250
- package/tests/policy-v15.test.mjs +234 -234
- package/tests/returned-handoff-notices.test.mjs +170 -0
- package/tests/runtime-v15.test.mjs +1197 -1191
- package/tests/task-slices.test.mjs +285 -285
- package/tests/uninstall.test.mjs +477 -0
- package/tests/unity-transport.test.mjs +348 -0
|
@@ -1,300 +1,346 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import crypto from "node:crypto";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import test from "node:test";
|
|
8
|
-
import { artifact, atomicJson, json } from "../.claude/nccgs/tools/policy.mjs";
|
|
9
|
-
import { initialize, snapshot, record, check, close, taskPath, reclassify, refreshInputs, importExternal, exportExternal, extendReview } from "../.claude/nccgs/tools/gates.mjs";
|
|
10
|
-
import { reserve, claim, complete, status, resume } from "../.claude/nccgs/tools/orchestration.mjs";
|
|
11
|
-
import { run } from "../.claude/nccgs/tools/task.mjs";
|
|
12
|
-
|
|
13
|
-
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
14
|
-
// All provider observations below are synthetic fixtures. They verify framework
|
|
15
|
-
// evidence mechanics, not actual Claude Code/model execution or game quality.
|
|
16
|
-
function fixture(options = {}) {
|
|
17
|
-
const project = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-lifecycle-v15-"));
|
|
18
|
-
const write = (relative, value) => {
|
|
19
|
-
const file = path.join(project, relative);
|
|
20
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
21
|
-
fs.writeFileSync(file, typeof value === "string" ? value : JSON.stringify(value));
|
|
22
|
-
return artifact(project, relative);
|
|
23
|
-
};
|
|
24
|
-
for (const name of ["pipeline.json", "studio.json"]) write(`.claude/nccgs/${name}`, fs.readFileSync(path.join(root, ".claude/nccgs", name), "utf8"));
|
|
25
|
-
write(".nccgs/project.yaml", "schema_version: 2\nmodels:\n profile: claude-standalone\n");
|
|
26
|
-
write(".nccgs/execution.json", { schemaVersion: 1, name: "standalone", externalReview: { mode: options.external || "off" }, acceptanceRequired: options.acceptance || false, requireObservedEffort: options.strictEffort || false });
|
|
27
|
-
write("Assets/game.cs", "// source baseline\n");
|
|
28
|
-
const contract = { schemaVersion: 1, objective: "Implement a bounded enemy feedback feature", mode: options.mode || "implement", scope: "feature", target: "production-change", readiness: "ready", requiredChecks: ["verification"], ...options.contract };
|
|
29
|
-
write(".nccgs/intake.json", contract);
|
|
30
|
-
initialize(project, "feature", null, null, null, { intake: ".nccgs/intake.json" });
|
|
31
|
-
let index = 0;
|
|
32
|
-
const task = () => json(taskPath(project, "feature"));
|
|
33
|
-
const snap = () => snapshot(project, task()).sha256;
|
|
34
|
-
function attempt(kind, rawOverrides = {}) {
|
|
35
|
-
const current = task(), count = ++index, agent = kind === "review" ? "nccgs-adversarial-reviewer" : kind === "analysis" ? "nccgs-task-scout" : "nccgs-gameplay-programmer";
|
|
36
|
-
const d = reserve(project, "feature", { kind, agent, requestKey: `request-${count}`, reason: `Synthetic fixture ${kind}`, writePaths: kind === "implementation" ? ["Assets/game.cs"] : [], limits: { maxDispatches: 100 } });
|
|
37
|
-
claim(project, d.id, { sessionId: "fixture-session", toolUseId: `tool-${count}`, agent });
|
|
38
|
-
const route = kind === "review" ? current.execution.profile.models.review : current.implementationRoute;
|
|
39
|
-
const model = route.model === "sonnet" ? "claude-sonnet-4-6" : route.model === "opus" ? "claude-opus-5-5" : route.model;
|
|
40
|
-
const source = `.nccgs/evidence/raw-${count}.json`;
|
|
41
|
-
write(source, { taskId: "feature", contractHash: current.contractHash, snapshot: snap(), dispatchId: d.id,
|
|
42
|
-
sessionId: "fixture-session", toolUseId: `tool-${count}`, agentId: `${kind}-${count}`, status: "completed", capturedAt: new Date().toISOString(),
|
|
43
|
-
observedProvider: "claude-code", observedModel: model, observedEffort: options.strictEffort ? route.effort : null,
|
|
44
|
-
provenance: { kind: "provider-log", reference: "SYNTHETIC TEST FIXTURE, not actual Claude Code execution" }, ...rawOverrides });
|
|
45
|
-
const runtime = run(["observe", "--project", project, "--id", "feature", "--file", source]);
|
|
46
|
-
complete(project, d.id, { runtime });
|
|
47
|
-
return { snapshot: snap(), dispatchId: d.id, runtime };
|
|
48
|
-
}
|
|
49
|
-
function work(overrides = {}, raw = {}) {
|
|
50
|
-
const name = task().mode === "implement" ? "implementation" : "analysis";
|
|
51
|
-
const input = { ...attempt(name, raw), checks: [{ name: "verification", status: "PASS", evidence: write(`.nccgs/evidence/check-${++index}.md`, "Fixture source reasoning / acceptance verification: PASS") }], ...overrides };
|
|
52
|
-
if (name === "analysis" && !input.evidence) input.evidence = write(task().contract.analysisArtifacts[0], "# Fixture analysis\n\nSource observations and recommendations.");
|
|
53
|
-
return record(project, "feature", name, input);
|
|
54
|
-
}
|
|
55
|
-
const review = (overrides = {}, raw = {}) => record(project, "feature", "review", { ...attempt("review", raw), verdict: "PASS", criteriaMet: true, findings: [], evidence: write(`.nccgs/evidence/review-${++index}.md`, "Fixture independent review: no open findings"), ...overrides });
|
|
56
|
-
const report = (outcome = "success", overrides = {}) => record(project, "feature", "report", { outcome, summary: `Fixture ${outcome} report. No real model/game execution claimed.`, ...overrides });
|
|
57
|
-
function receipt(overrides = {}) {
|
|
58
|
-
const relative = `.nccgs/evidence/receipt-${++index}.json`;
|
|
59
|
-
write(relative, { schemaVersion: 1, id: `receipt-${index}`, taskId: "feature", contractHash: task().contractHash, snapshot: snap(), actor: "External fixture reviewer", verdict: "PASS", findings: [], reviewedAt: new Date().toISOString(), source: "fixture external review event", ...overrides });
|
|
60
|
-
return relative;
|
|
61
|
-
}
|
|
62
|
-
return { project, contract, write, task, snap, attempt, work, review, report, receipt,
|
|
63
|
-
dispose() { const resolved = path.resolve(project); assert.ok(resolved.startsWith(path.resolve(os.tmpdir()) + path.sep + "nccgs-lifecycle-v15-")); fs.rmSync(resolved, { recursive: true, force: true }); } };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
test("standalone finishes without Astra or acceptance while preserving UNKNOWN effort", () => {
|
|
67
|
-
const f = fixture();
|
|
68
|
-
try {
|
|
69
|
-
assert.equal(f.task().schemaVersion, 2);
|
|
70
|
-
assert.equal(f.task().risk, "STANDARD");
|
|
71
|
-
assert.equal(f.task().implementationRoute.model, "opus");
|
|
72
|
-
assert.throws(() => close(f.project, "feature"), /implementation: missing/);
|
|
73
|
-
f.work(); f.review(); f.report();
|
|
74
|
-
const result = close(f.project, "feature");
|
|
75
|
-
assert.equal(result.phase, "DONE");
|
|
76
|
-
assert.equal(result.acceptanceRequired, false);
|
|
77
|
-
assert.equal(result.executionEvidence.effortEvidence, "UNKNOWN");
|
|
78
|
-
assert.equal(result.executionEvidence.observedEffort, null);
|
|
79
|
-
assert.deepEqual(Object.keys(f.task().stages), ["implementation", "review", "report"]);
|
|
80
|
-
} finally { f.dispose(); }
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("classifier uses concrete signals, rejects unsafe overrides and supports direct text intake", () => {
|
|
84
|
-
const fast = fixture({ contract: { objective: "Fix one UI label alignment", scope: "local", target: "fix" } });
|
|
85
|
-
const controlled = fixture({ contract: { objective: "Migrate the save serialization format", scope: "local" } });
|
|
86
|
-
try {
|
|
87
|
-
assert.equal(fast.task().risk, "FAST");
|
|
88
|
-
assert.equal(fast.task().implementationRoute.model, "sonnet");
|
|
89
|
-
assert.ok(fast.task().classification.evidence.length);
|
|
90
|
-
assert.equal(controlled.task().risk, "CONTROLLED");
|
|
91
|
-
assert.match(controlled.task().classification.reason, /save\/serialization|migration/);
|
|
92
|
-
assert.throws(() => reclassify(controlled.project, "feature", { risk: "FAST", reason: "Only one file", evidence: ["One file changed"] }), /require CONTROLLED/);
|
|
93
|
-
fast.write(".nccgs/request.md", "Review feasibility of the enemy feedback design. Produce a plan only.");
|
|
94
|
-
const planned = run(["init", "--project", fast.project, "--id", "direct-plan", "--intake", ".nccgs/request.md", "--mode", "plan"]);
|
|
95
|
-
assert.equal(planned.mode, "plan");
|
|
96
|
-
assert.equal(planned.risk, "STANDARD");
|
|
97
|
-
assert.deepEqual(planned.contract.analysisArtifacts, [".nccgs/evidence/analysis/direct-plan-result.md"]);
|
|
98
|
-
assert.throws(() => initialize(fast.project, "bad", null, null, "FAST", { intake: ".nccgs/intake.json" }), /classification reason/);
|
|
99
|
-
assert.throws(() => initialize(fast.project, "incomplete", null, null, "FAST", { intake: ".nccgs/request.md", reason: "Assumed small" }), /ready local fix/);
|
|
100
|
-
} finally { fast.dispose(); controlled.dispose(); }
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("audit and plan use analysis evidence and fail if source changes", () => {
|
|
104
|
-
for (const mode of ["audit", "plan"]) {
|
|
105
|
-
const f = fixture({ mode });
|
|
106
|
-
try {
|
|
107
|
-
assert.throws(() => reserve(f.project, "feature", { kind: "implementation", agent: "nccgs-gameplay-programmer", requestKey: "bad", reason: "Wrong mode", writePaths: ["Assets/game.cs"] }), /cannot dispatch implementation/);
|
|
108
|
-
const old = process.env.CLAUDECODE;
|
|
109
|
-
try {
|
|
110
|
-
process.env.CLAUDECODE = "1";
|
|
111
|
-
assert.throws(() => run(["verify", "--project", f.project, "--id", "feature", "--name", "verification", "--", process.execPath, "-e", "process.exit(99)"]), /cannot run arbitrary verification/);
|
|
112
|
-
} finally { if (old === undefined) delete process.env.CLAUDECODE; else process.env.CLAUDECODE = old; }
|
|
113
|
-
f.work(); f.review(); f.report();
|
|
114
|
-
assert.equal(close(f.project, "feature").phase, "DONE");
|
|
115
|
-
assert.equal(f.task().stages.implementation, undefined);
|
|
116
|
-
assert.ok(f.task().stages.analysis.evidence.path.startsWith(".nccgs/evidence/analysis/"));
|
|
117
|
-
f.write("Assets/game.cs", "// forbidden source edit\n");
|
|
118
|
-
assert.throws(() => close(f.project, "feature"), /modified source/);
|
|
119
|
-
f.report("blocked");
|
|
120
|
-
assert.equal(f.task().outcome, "blocked");
|
|
121
|
-
assert.throws(() => close(f.project, "feature"), /modified source/);
|
|
122
|
-
refreshInputs(f.project, "feature", "Recheck inputs without resetting source baseline");
|
|
123
|
-
assert.throws(() => check(f.project, f.task(), "analysis"), /modified source/);
|
|
124
|
-
} finally { f.dispose(); }
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("failed, blocked and partial reports are always writable without passing review or prerequisites", () => {
|
|
129
|
-
const f = fixture();
|
|
130
|
-
try {
|
|
131
|
-
for (const outcome of ["failed", "blocked", "partial"]) { f.report(outcome); assert.equal(f.task().stages.report.outcome, outcome); }
|
|
132
|
-
assert.equal(f.task().stages.report.verificationState.verified, false);
|
|
133
|
-
assert.throws(() => close(f.project, "feature"), /implementation: missing/);
|
|
134
|
-
f.work(); f.review({ verdict: "CHANGES_REQUIRED", criteriaMet: false, findings: [{ severity: "HIGH", status: "OPEN", description: "Fixture issue" }] });
|
|
135
|
-
const budget = f.task().reviewBudget.used;
|
|
136
|
-
f.report("blocked");
|
|
137
|
-
assert.equal(f.task().stages.review.findings[0].status, "OPEN");
|
|
138
|
-
assert.equal(f.task().reviewBudget.used, budget);
|
|
139
|
-
assert.throws(() => close(f.project, "feature"), /not passed/);
|
|
140
|
-
fs.unlinkSync(path.join(f.project, ".nccgs/intake.json"));
|
|
141
|
-
f.report("failed");
|
|
142
|
-
assert.equal(f.task().outcome, "failed");
|
|
143
|
-
const inspected = run(["inspect", "--project", f.project, "--id", "feature"]);
|
|
144
|
-
assert.equal(inspected.inputValidation.allowed, false);
|
|
145
|
-
assert.equal(inspected.task.outcome, "failed");
|
|
146
|
-
f.write(".nccgs/execution.json", { schemaVersion: 1, name: "standalone", externalReview: { mode: "required" } });
|
|
147
|
-
f.report("blocked");
|
|
148
|
-
assert.throws(() => close(f.project, "feature"), /Execution policy changed/);
|
|
149
|
-
f.write(".nccgs/execution.json", "malformed policy");
|
|
150
|
-
const failure = run(["report", "--project", f.project, "--id", "feature", "--outcome", "failed", "--summary", "Execution policy is unreadable; work cannot continue"]);
|
|
151
|
-
assert.equal(failure.outcome, "failed");
|
|
152
|
-
assert.equal(run(["inspect", "--project", f.project, "--id", "feature"]).inputValidation.allowed, false);
|
|
153
|
-
} finally { f.dispose(); }
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test("standalone rejects wrong model and same reviewer identity; completed dispatch cannot be reused", () => {
|
|
157
|
-
const f = fixture();
|
|
158
|
-
try {
|
|
159
|
-
assert.throws(() => f.work({}, { observedModel: "claude-sonnet-4-6" }), /model\/fallback/);
|
|
160
|
-
f.work();
|
|
161
|
-
const implementation = f.task().stages.implementation;
|
|
162
|
-
assert.throws(() => record(f.project, "feature", "implementation", implementation), /only once/);
|
|
163
|
-
const trace = json(path.join(f.project, implementation.runtime.path));
|
|
164
|
-
assert.throws(() => f.review({}, { agentId: trace.agentId }), /independent/);
|
|
165
|
-
assert.equal(f.task().reviewBudget.used, 0);
|
|
166
|
-
f.review();
|
|
167
|
-
const reviewed = f.task().stages.review;
|
|
168
|
-
assert.throws(() => record(f.project, "feature", "review", reviewed), /only once/);
|
|
169
|
-
assert.equal(f.task().reviewBudget.used, 1);
|
|
170
|
-
} finally { f.dispose(); }
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test("missing Claude Code provider/model and mandatory effort cannot become verified claims", () => {
|
|
174
|
-
const strict = fixture({ strictEffort: true });
|
|
175
|
-
const provider = fixture();
|
|
176
|
-
const model = fixture();
|
|
177
|
-
try {
|
|
178
|
-
assert.throws(() => strict.work({}, { observedEffort: null }), /effort is UNKNOWN/);
|
|
179
|
-
assert.equal(strict.task().stages.implementation, undefined);
|
|
180
|
-
assert.throws(() => provider.attempt("implementation", { observedProvider: null }), /observed Claude Code provider/);
|
|
181
|
-
assert.throws(() => model.attempt("implementation", { observedModel: null }), /Incomplete observed/);
|
|
182
|
-
strict.report("blocked"); provider.report("blocked"); model.report("blocked");
|
|
183
|
-
assert.equal(strict.task().outcome, "blocked");
|
|
184
|
-
} finally { strict.dispose(); provider.dispose(); model.dispose(); }
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test("reclassification, input refresh and resume retain attempts, review budget and evidence history", () => {
|
|
188
|
-
const f = fixture({ contract: { objective: "Fix a local UI text", scope: "local", target: "fix" } });
|
|
189
|
-
try {
|
|
190
|
-
f.work(); f.review({ verdict: "CHANGES_REQUIRED", criteriaMet: false });
|
|
191
|
-
const original = f.task(), attempts = status(f.project, "feature").usage.dispatches;
|
|
192
|
-
reclassify(f.project, "feature", { risk: "CONTROLLED", reason: "Inspection found additional integration uncertainty", evidence: [{ observation: "Two interacting update paths found" }] });
|
|
193
|
-
assert.notEqual(f.task().contractHash, original.contractHash);
|
|
194
|
-
assert.equal(f.task().implementationRoute.effort, "
|
|
195
|
-
assert.equal(f.task().reviewBudget.used, 1);
|
|
196
|
-
assert.equal(f.task().reviewBudget.limit, 2);
|
|
197
|
-
assert.deepEqual(f.task().stages, {});
|
|
198
|
-
assert.ok(f.task().history.some(item => item.stages?.review?.verdict === "CHANGES_REQUIRED"));
|
|
199
|
-
f.write(".nccgs/requirements.yaml", "new requirement: preserve feedback timing\n");
|
|
200
|
-
assert.throws(() => check(f.project, f.task(), "implementation"), /Canonical inputs are stale/);
|
|
201
|
-
f.report("blocked");
|
|
202
|
-
refreshInputs(f.project, "feature", "Reconcile changed requirement source");
|
|
203
|
-
assert.equal(f.task().reviewBudget.used, 1);
|
|
204
|
-
assert.equal(f.task().reviewBudget.limit, 2);
|
|
205
|
-
assert.equal(f.task().risk, "CONTROLLED");
|
|
206
|
-
resume(f.project, "feature");
|
|
207
|
-
assert.equal(status(f.project, "feature").usage.dispatches, attempts);
|
|
208
|
-
assert.equal(f.task().reviewBudget.used, 1);
|
|
209
|
-
} finally { f.dispose(); }
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test("contract source files and acceptance mappings stay enforced after intake normalization", () => {
|
|
213
|
-
const f = fixture();
|
|
214
|
-
try {
|
|
215
|
-
f.write(".nccgs/design.md", "Current damage feedback requirements");
|
|
216
|
-
f.write(".nccgs/intake.json", { ...f.contract, sources: [".nccgs/design.md"], acceptance: [{ id: "readability", description: "Player reads hit feedback", manual: true }, { id: "regression", description: "Old damage behavior preserved", check: "damage-regression" }] });
|
|
217
|
-
refreshInputs(f.project, "feature", "Include source and explicit acceptance mapping");
|
|
218
|
-
assert.ok(f.task().contract.requiredChecks.includes("damage-regression"));
|
|
219
|
-
assert.ok(f.task().contract.manualGates.includes("readability"));
|
|
220
|
-
f.write(".nccgs/design.md", "Changed requirements");
|
|
221
|
-
assert.throws(() => check(f.project, f.task(), "implementation"), /stale artifact/);
|
|
222
|
-
f.report("blocked");
|
|
223
|
-
assert.equal(f.task().stages.report.verificationState.verified, false);
|
|
224
|
-
f.write(".nccgs/invalid.json", { ...f.contract, acceptance: [{ id: "unmapped", description: "Cannot silently ignore" }] });
|
|
225
|
-
assert.throws(() => initialize(f.project, "bad-criteria", null, null, null, { contract: ".nccgs/invalid.json" }), /check name or manual/);
|
|
226
|
-
} finally { f.dispose(); }
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
test("manual acceptance gates require real external decision data only when declared", () => {
|
|
230
|
-
const f = fixture({ contract: { manualGates: ["playtest"] } });
|
|
231
|
-
try {
|
|
232
|
-
f.work(); f.review(); f.report();
|
|
233
|
-
assert.equal(check(f.project, f.task(), "acceptance").acceptanceRequired, true);
|
|
234
|
-
assert.throws(() => close(f.project, "feature"), /acceptance: missing/);
|
|
235
|
-
const approve = (overrides = {}) => record(f.project, "feature", "acceptance", { snapshot: f.snap(), evidence: f.write(`.nccgs/evidence/approval-${crypto.randomUUID()}.json`, { actor: "Product Owner", decision: "ACCEPTED", taskId: "feature", snapshot: f.snap(), contractHash: f.task().contractHash, decidedAt: new Date().toISOString(), statement: "Fixture explicit decision", reference: "fixture human decision", gates: [{ id: "playtest", decision: "ACCEPTED", reference: "fixture playtest" }], ...overrides }) });
|
|
236
|
-
assert.throws(() => approve({ actor: "Internal reviewer" }), /Product Owner/);
|
|
237
|
-
assert.throws(() => approve({ gates: [] }), /Manual gate missing/);
|
|
238
|
-
approve();
|
|
239
|
-
assert.equal(close(f.project, "feature").phase, "DONE");
|
|
240
|
-
} finally { f.dispose(); }
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test("external off, advisory and required enforce distinct gates and retain findings", () => {
|
|
244
|
-
const off = fixture(), advisory = fixture({ external: "advisory" }), required = fixture({ external: "required" });
|
|
245
|
-
try {
|
|
246
|
-
assert.throws(() => importExternal(off.project, "feature", off.receipt()), /review is off/);
|
|
247
|
-
for (const f of [advisory, required]) { f.work(); f.review(); f.report(); }
|
|
248
|
-
const receipt = advisory.receipt({ verdict: "CHANGES_REQUIRED", findings: [{ severity: "HIGH", status: "OPEN", description: "Unconfirmed fixture concern" }] });
|
|
249
|
-
importExternal(advisory.project, "feature", receipt);
|
|
250
|
-
assert.equal(close(advisory.project, "feature").phase, "DONE");
|
|
251
|
-
assert.equal(check(advisory.project, advisory.task(), "done").externalReview.findings[0].status, "OPEN");
|
|
252
|
-
assert.throws(() => close(required.project, "feature"), /Required external review is missing/);
|
|
253
|
-
const exported = exportExternal(required.project, "feature", ".nccgs/evidence/external/requests/request.json");
|
|
254
|
-
assert.equal(json(path.join(required.project, exported.path)).contractHash, required.task().contractHash);
|
|
255
|
-
assert.throws(() => exportExternal(required.project, "feature", exported.path), /EEXIST/);
|
|
256
|
-
assert.throws(() => exportExternal(required.project, "feature", required.task().stages.implementation.runtime.path), /external\/requests/);
|
|
257
|
-
const blockedReceipt = required.receipt({ verdict: "BLOCKED", findings: [{ severity: "HIGH", status: "OPEN", description: "Must resolve fixture risk" }] });
|
|
258
|
-
importExternal(required.project, "feature", blockedReceipt);
|
|
259
|
-
assert.throws(() => close(required.project, "feature"), /has not passed/);
|
|
260
|
-
importExternal(required.project, "feature", required.receipt());
|
|
261
|
-
assert.throws(() => close(required.project, "feature"), /explicit supersession/);
|
|
262
|
-
importExternal(required.project, "feature", required.receipt({ supersedes: [json(path.join(required.project, blockedReceipt)).id], supersessionReason: "External reviewer verified the prior issue is resolved; fixture source report" }));
|
|
263
|
-
assert.equal(close(required.project, "feature").phase, "DONE");
|
|
264
|
-
assert.ok(check(required.project, required.task(), "done").externalReview.findings[0].supersededBy);
|
|
265
|
-
} finally { off.dispose(); advisory.dispose(); required.dispose(); }
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
test("external receipts reject stale contracts, stale snapshots and duplicate IDs with different content", () => {
|
|
269
|
-
const f = fixture({ external: "required" });
|
|
270
|
-
try {
|
|
271
|
-
const stale = f.receipt();
|
|
272
|
-
reclassify(f.project, "feature", { risk: "CONTROLLED", reason: "Additional risk found", evidence: ["fixture inspection source"] });
|
|
273
|
-
assert.throws(() => importExternal(f.project, "feature", stale), /stale/);
|
|
274
|
-
assert.throws(() => importExternal(f.project, "feature", f.receipt({ snapshot: "different" })), /stale/);
|
|
275
|
-
assert.throws(() => importExternal(f.project, "feature", f.receipt({ findings: [{}] })), /malformed findings/);
|
|
276
|
-
const current = f.receipt({ id: "external-123" });
|
|
277
|
-
importExternal(f.project, "feature", current);
|
|
278
|
-
importExternal(f.project, "feature", current);
|
|
279
|
-
assert.equal(f.task().externalReviews.length, 1);
|
|
280
|
-
assert.throws(() => importExternal(f.project, "feature", f.receipt({ id: "external-123", verdict: "BLOCKED" })), /different content/);
|
|
281
|
-
const old = process.env.CLAUDECODE;
|
|
282
|
-
try { process.env.CLAUDECODE = "1"; assert.throws(() => importExternal(f.project, "feature", current), /outside the Claude session/); }
|
|
283
|
-
finally { if (old === undefined) delete process.env.CLAUDECODE; else process.env.CLAUDECODE = old; }
|
|
284
|
-
} finally { f.dispose(); }
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
test("partial reports and missing required closure artifacts never unlock DONE", () => {
|
|
288
|
-
const f = fixture({ contract: { requiredArtifacts: [".nccgs/evidence/build.txt"] } });
|
|
289
|
-
try {
|
|
290
|
-
f.work(); f.review(); f.report("partial");
|
|
291
|
-
assert.throws(() => close(f.project, "feature"), /not success/);
|
|
292
|
-
f.report("success");
|
|
293
|
-
assert.throws(() => close(f.project, "feature"), /closure artifact missing/);
|
|
294
|
-
f.report("blocked");
|
|
295
|
-
f.report("success", { artifacts: [f.write(".nccgs/evidence/build.txt", "fixture build artifact")] });
|
|
296
|
-
assert.equal(close(f.project, "feature").phase, "DONE");
|
|
297
|
-
f.write(".nccgs/evidence/build.txt", "changed artifact");
|
|
298
|
-
assert.throws(() => close(f.project, "feature"), /stale artifact/);
|
|
299
|
-
} finally { f.dispose(); }
|
|
300
|
-
});
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import { artifact, atomicJson, json } from "../.claude/nccgs/tools/policy.mjs";
|
|
9
|
+
import { initialize, snapshot, record, check, close, taskPath, reclassify, refreshInputs, importExternal, exportExternal, extendReview } from "../.claude/nccgs/tools/gates.mjs";
|
|
10
|
+
import { reserve, claim, complete, status, resume } from "../.claude/nccgs/tools/orchestration.mjs";
|
|
11
|
+
import { run } from "../.claude/nccgs/tools/task.mjs";
|
|
12
|
+
|
|
13
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
14
|
+
// All provider observations below are synthetic fixtures. They verify framework
|
|
15
|
+
// evidence mechanics, not actual Claude Code/model execution or game quality.
|
|
16
|
+
function fixture(options = {}) {
|
|
17
|
+
const project = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-lifecycle-v15-"));
|
|
18
|
+
const write = (relative, value) => {
|
|
19
|
+
const file = path.join(project, relative);
|
|
20
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
21
|
+
fs.writeFileSync(file, typeof value === "string" ? value : JSON.stringify(value));
|
|
22
|
+
return artifact(project, relative);
|
|
23
|
+
};
|
|
24
|
+
for (const name of ["pipeline.json", "studio.json"]) write(`.claude/nccgs/${name}`, fs.readFileSync(path.join(root, ".claude/nccgs", name), "utf8"));
|
|
25
|
+
write(".nccgs/project.yaml", "schema_version: 2\nmodels:\n profile: claude-standalone\n");
|
|
26
|
+
write(".nccgs/execution.json", { schemaVersion: 1, name: "standalone", externalReview: { mode: options.external || "off" }, acceptanceRequired: options.acceptance || false, requireObservedEffort: options.strictEffort || false });
|
|
27
|
+
write("Assets/game.cs", "// source baseline\n");
|
|
28
|
+
const contract = { schemaVersion: 1, objective: "Implement a bounded enemy feedback feature", mode: options.mode || "implement", scope: "feature", target: "production-change", readiness: "ready", requiredChecks: ["verification"], ...options.contract };
|
|
29
|
+
write(".nccgs/intake.json", contract);
|
|
30
|
+
initialize(project, "feature", null, null, null, { intake: ".nccgs/intake.json" });
|
|
31
|
+
let index = 0;
|
|
32
|
+
const task = () => json(taskPath(project, "feature"));
|
|
33
|
+
const snap = () => snapshot(project, task()).sha256;
|
|
34
|
+
function attempt(kind, rawOverrides = {}) {
|
|
35
|
+
const current = task(), count = ++index, agent = kind === "review" ? "nccgs-adversarial-reviewer" : kind === "analysis" ? "nccgs-task-scout" : current.implementationRoute.class === "deep" ? "nccgs-unity-implementer" : "nccgs-gameplay-programmer";
|
|
36
|
+
const d = reserve(project, "feature", { kind, agent, requestKey: `request-${count}`, reason: `Synthetic fixture ${kind}`, writePaths: kind === "implementation" ? ["Assets/game.cs"] : [], limits: { maxDispatches: 100 } });
|
|
37
|
+
claim(project, d.id, { sessionId: "fixture-session", toolUseId: `tool-${count}`, agent });
|
|
38
|
+
const route = kind === "review" ? current.execution.profile.models.review : current.implementationRoute;
|
|
39
|
+
const model = route.model === "sonnet" ? "claude-sonnet-4-6" : route.model === "opus" ? "claude-opus-5-5" : route.model === "fable" ? "claude-fable-5-1" : route.model;
|
|
40
|
+
const source = `.nccgs/evidence/raw-${count}.json`;
|
|
41
|
+
write(source, { taskId: "feature", contractHash: current.contractHash, snapshot: snap(), dispatchId: d.id,
|
|
42
|
+
sessionId: "fixture-session", toolUseId: `tool-${count}`, agentId: `${kind}-${count}`, status: "completed", capturedAt: new Date().toISOString(),
|
|
43
|
+
observedProvider: "claude-code", observedModel: model, observedEffort: options.strictEffort ? route.effort : null,
|
|
44
|
+
provenance: { kind: "provider-log", reference: "SYNTHETIC TEST FIXTURE, not actual Claude Code execution" }, ...rawOverrides });
|
|
45
|
+
const runtime = run(["observe", "--project", project, "--id", "feature", "--file", source]);
|
|
46
|
+
complete(project, d.id, { runtime });
|
|
47
|
+
return { snapshot: snap(), dispatchId: d.id, runtime };
|
|
48
|
+
}
|
|
49
|
+
function work(overrides = {}, raw = {}) {
|
|
50
|
+
const name = task().mode === "implement" ? "implementation" : "analysis";
|
|
51
|
+
const input = { ...attempt(name, raw), checks: [{ name: "verification", status: "PASS", evidence: write(`.nccgs/evidence/check-${++index}.md`, "Fixture source reasoning / acceptance verification: PASS") }], ...overrides };
|
|
52
|
+
if (name === "analysis" && !input.evidence) input.evidence = write(task().contract.analysisArtifacts[0], "# Fixture analysis\n\nSource observations and recommendations.");
|
|
53
|
+
return record(project, "feature", name, input);
|
|
54
|
+
}
|
|
55
|
+
const review = (overrides = {}, raw = {}) => record(project, "feature", "review", { ...attempt("review", raw), verdict: "PASS", criteriaMet: true, findings: [], evidence: write(`.nccgs/evidence/review-${++index}.md`, "Fixture independent review: no open findings"), ...overrides });
|
|
56
|
+
const report = (outcome = "success", overrides = {}) => record(project, "feature", "report", { outcome, summary: `Fixture ${outcome} report. No real model/game execution claimed.`, ...overrides });
|
|
57
|
+
function receipt(overrides = {}) {
|
|
58
|
+
const relative = `.nccgs/evidence/receipt-${++index}.json`;
|
|
59
|
+
write(relative, { schemaVersion: 1, id: `receipt-${index}`, taskId: "feature", contractHash: task().contractHash, snapshot: snap(), actor: "External fixture reviewer", verdict: "PASS", findings: [], reviewedAt: new Date().toISOString(), source: "fixture external review event", ...overrides });
|
|
60
|
+
return relative;
|
|
61
|
+
}
|
|
62
|
+
return { project, contract, write, task, snap, attempt, work, review, report, receipt,
|
|
63
|
+
dispose() { const resolved = path.resolve(project); assert.ok(resolved.startsWith(path.resolve(os.tmpdir()) + path.sep + "nccgs-lifecycle-v15-")); fs.rmSync(resolved, { recursive: true, force: true }); } };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
test("standalone finishes without Astra or acceptance while preserving UNKNOWN effort", () => {
|
|
67
|
+
const f = fixture();
|
|
68
|
+
try {
|
|
69
|
+
assert.equal(f.task().schemaVersion, 2);
|
|
70
|
+
assert.equal(f.task().risk, "STANDARD");
|
|
71
|
+
assert.equal(f.task().implementationRoute.model, "opus");
|
|
72
|
+
assert.throws(() => close(f.project, "feature"), /implementation: missing/);
|
|
73
|
+
f.work(); f.review(); f.report();
|
|
74
|
+
const result = close(f.project, "feature");
|
|
75
|
+
assert.equal(result.phase, "DONE");
|
|
76
|
+
assert.equal(result.acceptanceRequired, false);
|
|
77
|
+
assert.equal(result.executionEvidence.effortEvidence, "UNKNOWN");
|
|
78
|
+
assert.equal(result.executionEvidence.observedEffort, null);
|
|
79
|
+
assert.deepEqual(Object.keys(f.task().stages), ["implementation", "review", "report"]);
|
|
80
|
+
} finally { f.dispose(); }
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("classifier uses concrete signals, rejects unsafe overrides and supports direct text intake", () => {
|
|
84
|
+
const fast = fixture({ contract: { objective: "Fix one UI label alignment", scope: "local", target: "fix" } });
|
|
85
|
+
const controlled = fixture({ contract: { objective: "Migrate the save serialization format", scope: "local" } });
|
|
86
|
+
try {
|
|
87
|
+
assert.equal(fast.task().risk, "FAST");
|
|
88
|
+
assert.equal(fast.task().implementationRoute.model, "sonnet");
|
|
89
|
+
assert.ok(fast.task().classification.evidence.length);
|
|
90
|
+
assert.equal(controlled.task().risk, "CONTROLLED");
|
|
91
|
+
assert.match(controlled.task().classification.reason, /save\/serialization|migration/);
|
|
92
|
+
assert.throws(() => reclassify(controlled.project, "feature", { risk: "FAST", reason: "Only one file", evidence: ["One file changed"] }), /require CONTROLLED/);
|
|
93
|
+
fast.write(".nccgs/request.md", "Review feasibility of the enemy feedback design. Produce a plan only.");
|
|
94
|
+
const planned = run(["init", "--project", fast.project, "--id", "direct-plan", "--intake", ".nccgs/request.md", "--mode", "plan"]);
|
|
95
|
+
assert.equal(planned.mode, "plan");
|
|
96
|
+
assert.equal(planned.risk, "STANDARD");
|
|
97
|
+
assert.deepEqual(planned.contract.analysisArtifacts, [".nccgs/evidence/analysis/direct-plan-result.md"]);
|
|
98
|
+
assert.throws(() => initialize(fast.project, "bad", null, null, "FAST", { intake: ".nccgs/intake.json" }), /classification reason/);
|
|
99
|
+
assert.throws(() => initialize(fast.project, "incomplete", null, null, "FAST", { intake: ".nccgs/request.md", reason: "Assumed small" }), /ready local fix/);
|
|
100
|
+
} finally { fast.dispose(); controlled.dispose(); }
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("audit and plan use analysis evidence and fail if source changes", () => {
|
|
104
|
+
for (const mode of ["audit", "plan"]) {
|
|
105
|
+
const f = fixture({ mode });
|
|
106
|
+
try {
|
|
107
|
+
assert.throws(() => reserve(f.project, "feature", { kind: "implementation", agent: "nccgs-gameplay-programmer", requestKey: "bad", reason: "Wrong mode", writePaths: ["Assets/game.cs"] }), /cannot dispatch implementation/);
|
|
108
|
+
const old = process.env.CLAUDECODE;
|
|
109
|
+
try {
|
|
110
|
+
process.env.CLAUDECODE = "1";
|
|
111
|
+
assert.throws(() => run(["verify", "--project", f.project, "--id", "feature", "--name", "verification", "--", process.execPath, "-e", "process.exit(99)"]), /cannot run arbitrary verification/);
|
|
112
|
+
} finally { if (old === undefined) delete process.env.CLAUDECODE; else process.env.CLAUDECODE = old; }
|
|
113
|
+
f.work(); f.review(); f.report();
|
|
114
|
+
assert.equal(close(f.project, "feature").phase, "DONE");
|
|
115
|
+
assert.equal(f.task().stages.implementation, undefined);
|
|
116
|
+
assert.ok(f.task().stages.analysis.evidence.path.startsWith(".nccgs/evidence/analysis/"));
|
|
117
|
+
f.write("Assets/game.cs", "// forbidden source edit\n");
|
|
118
|
+
assert.throws(() => close(f.project, "feature"), /modified source/);
|
|
119
|
+
f.report("blocked");
|
|
120
|
+
assert.equal(f.task().outcome, "blocked");
|
|
121
|
+
assert.throws(() => close(f.project, "feature"), /modified source/);
|
|
122
|
+
refreshInputs(f.project, "feature", "Recheck inputs without resetting source baseline");
|
|
123
|
+
assert.throws(() => check(f.project, f.task(), "analysis"), /modified source/);
|
|
124
|
+
} finally { f.dispose(); }
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("failed, blocked and partial reports are always writable without passing review or prerequisites", () => {
|
|
129
|
+
const f = fixture();
|
|
130
|
+
try {
|
|
131
|
+
for (const outcome of ["failed", "blocked", "partial"]) { f.report(outcome); assert.equal(f.task().stages.report.outcome, outcome); }
|
|
132
|
+
assert.equal(f.task().stages.report.verificationState.verified, false);
|
|
133
|
+
assert.throws(() => close(f.project, "feature"), /implementation: missing/);
|
|
134
|
+
f.work(); f.review({ verdict: "CHANGES_REQUIRED", criteriaMet: false, findings: [{ severity: "HIGH", status: "OPEN", description: "Fixture issue" }] });
|
|
135
|
+
const budget = f.task().reviewBudget.used;
|
|
136
|
+
f.report("blocked");
|
|
137
|
+
assert.equal(f.task().stages.review.findings[0].status, "OPEN");
|
|
138
|
+
assert.equal(f.task().reviewBudget.used, budget);
|
|
139
|
+
assert.throws(() => close(f.project, "feature"), /not passed/);
|
|
140
|
+
fs.unlinkSync(path.join(f.project, ".nccgs/intake.json"));
|
|
141
|
+
f.report("failed");
|
|
142
|
+
assert.equal(f.task().outcome, "failed");
|
|
143
|
+
const inspected = run(["inspect", "--project", f.project, "--id", "feature"]);
|
|
144
|
+
assert.equal(inspected.inputValidation.allowed, false);
|
|
145
|
+
assert.equal(inspected.task.outcome, "failed");
|
|
146
|
+
f.write(".nccgs/execution.json", { schemaVersion: 1, name: "standalone", externalReview: { mode: "required" } });
|
|
147
|
+
f.report("blocked");
|
|
148
|
+
assert.throws(() => close(f.project, "feature"), /Execution policy changed/);
|
|
149
|
+
f.write(".nccgs/execution.json", "malformed policy");
|
|
150
|
+
const failure = run(["report", "--project", f.project, "--id", "feature", "--outcome", "failed", "--summary", "Execution policy is unreadable; work cannot continue"]);
|
|
151
|
+
assert.equal(failure.outcome, "failed");
|
|
152
|
+
assert.equal(run(["inspect", "--project", f.project, "--id", "feature"]).inputValidation.allowed, false);
|
|
153
|
+
} finally { f.dispose(); }
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("standalone rejects wrong model and same reviewer identity; completed dispatch cannot be reused", () => {
|
|
157
|
+
const f = fixture();
|
|
158
|
+
try {
|
|
159
|
+
assert.throws(() => f.work({}, { observedModel: "claude-sonnet-4-6" }), /model\/fallback/);
|
|
160
|
+
f.work();
|
|
161
|
+
const implementation = f.task().stages.implementation;
|
|
162
|
+
assert.throws(() => record(f.project, "feature", "implementation", implementation), /only once/);
|
|
163
|
+
const trace = json(path.join(f.project, implementation.runtime.path));
|
|
164
|
+
assert.throws(() => f.review({}, { agentId: trace.agentId }), /independent/);
|
|
165
|
+
assert.equal(f.task().reviewBudget.used, 0);
|
|
166
|
+
f.review();
|
|
167
|
+
const reviewed = f.task().stages.review;
|
|
168
|
+
assert.throws(() => record(f.project, "feature", "review", reviewed), /only once/);
|
|
169
|
+
assert.equal(f.task().reviewBudget.used, 1);
|
|
170
|
+
} finally { f.dispose(); }
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("missing Claude Code provider/model and mandatory effort cannot become verified claims", () => {
|
|
174
|
+
const strict = fixture({ strictEffort: true });
|
|
175
|
+
const provider = fixture();
|
|
176
|
+
const model = fixture();
|
|
177
|
+
try {
|
|
178
|
+
assert.throws(() => strict.work({}, { observedEffort: null }), /effort is UNKNOWN/);
|
|
179
|
+
assert.equal(strict.task().stages.implementation, undefined);
|
|
180
|
+
assert.throws(() => provider.attempt("implementation", { observedProvider: null }), /observed Claude Code provider/);
|
|
181
|
+
assert.throws(() => model.attempt("implementation", { observedModel: null }), /Incomplete observed/);
|
|
182
|
+
strict.report("blocked"); provider.report("blocked"); model.report("blocked");
|
|
183
|
+
assert.equal(strict.task().outcome, "blocked");
|
|
184
|
+
} finally { strict.dispose(); provider.dispose(); model.dispose(); }
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("reclassification, input refresh and resume retain attempts, review budget and evidence history", () => {
|
|
188
|
+
const f = fixture({ contract: { objective: "Fix a local UI text", scope: "local", target: "fix" } });
|
|
189
|
+
try {
|
|
190
|
+
f.work(); f.review({ verdict: "CHANGES_REQUIRED", criteriaMet: false });
|
|
191
|
+
const original = f.task(), attempts = status(f.project, "feature").usage.dispatches;
|
|
192
|
+
reclassify(f.project, "feature", { risk: "CONTROLLED", reason: "Inspection found additional integration uncertainty", evidence: [{ observation: "Two interacting update paths found" }] });
|
|
193
|
+
assert.notEqual(f.task().contractHash, original.contractHash);
|
|
194
|
+
assert.equal(f.task().implementationRoute.effort, "xhigh");
|
|
195
|
+
assert.equal(f.task().reviewBudget.used, 1);
|
|
196
|
+
assert.equal(f.task().reviewBudget.limit, 2);
|
|
197
|
+
assert.deepEqual(f.task().stages, {});
|
|
198
|
+
assert.ok(f.task().history.some(item => item.stages?.review?.verdict === "CHANGES_REQUIRED"));
|
|
199
|
+
f.write(".nccgs/requirements.yaml", "new requirement: preserve feedback timing\n");
|
|
200
|
+
assert.throws(() => check(f.project, f.task(), "implementation"), /Canonical inputs are stale/);
|
|
201
|
+
f.report("blocked");
|
|
202
|
+
refreshInputs(f.project, "feature", "Reconcile changed requirement source");
|
|
203
|
+
assert.equal(f.task().reviewBudget.used, 1);
|
|
204
|
+
assert.equal(f.task().reviewBudget.limit, 2);
|
|
205
|
+
assert.equal(f.task().risk, "CONTROLLED");
|
|
206
|
+
resume(f.project, "feature");
|
|
207
|
+
assert.equal(status(f.project, "feature").usage.dispatches, attempts);
|
|
208
|
+
assert.equal(f.task().reviewBudget.used, 1);
|
|
209
|
+
} finally { f.dispose(); }
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("contract source files and acceptance mappings stay enforced after intake normalization", () => {
|
|
213
|
+
const f = fixture();
|
|
214
|
+
try {
|
|
215
|
+
f.write(".nccgs/design.md", "Current damage feedback requirements");
|
|
216
|
+
f.write(".nccgs/intake.json", { ...f.contract, sources: [".nccgs/design.md"], acceptance: [{ id: "readability", description: "Player reads hit feedback", manual: true }, { id: "regression", description: "Old damage behavior preserved", check: "damage-regression" }] });
|
|
217
|
+
refreshInputs(f.project, "feature", "Include source and explicit acceptance mapping");
|
|
218
|
+
assert.ok(f.task().contract.requiredChecks.includes("damage-regression"));
|
|
219
|
+
assert.ok(f.task().contract.manualGates.includes("readability"));
|
|
220
|
+
f.write(".nccgs/design.md", "Changed requirements");
|
|
221
|
+
assert.throws(() => check(f.project, f.task(), "implementation"), /stale artifact/);
|
|
222
|
+
f.report("blocked");
|
|
223
|
+
assert.equal(f.task().stages.report.verificationState.verified, false);
|
|
224
|
+
f.write(".nccgs/invalid.json", { ...f.contract, acceptance: [{ id: "unmapped", description: "Cannot silently ignore" }] });
|
|
225
|
+
assert.throws(() => initialize(f.project, "bad-criteria", null, null, null, { contract: ".nccgs/invalid.json" }), /check name or manual/);
|
|
226
|
+
} finally { f.dispose(); }
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("manual acceptance gates require real external decision data only when declared", () => {
|
|
230
|
+
const f = fixture({ contract: { manualGates: ["playtest"] } });
|
|
231
|
+
try {
|
|
232
|
+
f.work(); f.review(); f.report();
|
|
233
|
+
assert.equal(check(f.project, f.task(), "acceptance").acceptanceRequired, true);
|
|
234
|
+
assert.throws(() => close(f.project, "feature"), /acceptance: missing/);
|
|
235
|
+
const approve = (overrides = {}) => record(f.project, "feature", "acceptance", { snapshot: f.snap(), evidence: f.write(`.nccgs/evidence/approval-${crypto.randomUUID()}.json`, { actor: "Product Owner", decision: "ACCEPTED", taskId: "feature", snapshot: f.snap(), contractHash: f.task().contractHash, decidedAt: new Date().toISOString(), statement: "Fixture explicit decision", reference: "fixture human decision", gates: [{ id: "playtest", decision: "ACCEPTED", reference: "fixture playtest" }], ...overrides }) });
|
|
236
|
+
assert.throws(() => approve({ actor: "Internal reviewer" }), /Product Owner/);
|
|
237
|
+
assert.throws(() => approve({ gates: [] }), /Manual gate missing/);
|
|
238
|
+
approve();
|
|
239
|
+
assert.equal(close(f.project, "feature").phase, "DONE");
|
|
240
|
+
} finally { f.dispose(); }
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("external off, advisory and required enforce distinct gates and retain findings", () => {
|
|
244
|
+
const off = fixture(), advisory = fixture({ external: "advisory" }), required = fixture({ external: "required" });
|
|
245
|
+
try {
|
|
246
|
+
assert.throws(() => importExternal(off.project, "feature", off.receipt()), /review is off/);
|
|
247
|
+
for (const f of [advisory, required]) { f.work(); f.review(); f.report(); }
|
|
248
|
+
const receipt = advisory.receipt({ verdict: "CHANGES_REQUIRED", findings: [{ severity: "HIGH", status: "OPEN", description: "Unconfirmed fixture concern" }] });
|
|
249
|
+
importExternal(advisory.project, "feature", receipt);
|
|
250
|
+
assert.equal(close(advisory.project, "feature").phase, "DONE");
|
|
251
|
+
assert.equal(check(advisory.project, advisory.task(), "done").externalReview.findings[0].status, "OPEN");
|
|
252
|
+
assert.throws(() => close(required.project, "feature"), /Required external review is missing/);
|
|
253
|
+
const exported = exportExternal(required.project, "feature", ".nccgs/evidence/external/requests/request.json");
|
|
254
|
+
assert.equal(json(path.join(required.project, exported.path)).contractHash, required.task().contractHash);
|
|
255
|
+
assert.throws(() => exportExternal(required.project, "feature", exported.path), /EEXIST/);
|
|
256
|
+
assert.throws(() => exportExternal(required.project, "feature", required.task().stages.implementation.runtime.path), /external\/requests/);
|
|
257
|
+
const blockedReceipt = required.receipt({ verdict: "BLOCKED", findings: [{ severity: "HIGH", status: "OPEN", description: "Must resolve fixture risk" }] });
|
|
258
|
+
importExternal(required.project, "feature", blockedReceipt);
|
|
259
|
+
assert.throws(() => close(required.project, "feature"), /has not passed/);
|
|
260
|
+
importExternal(required.project, "feature", required.receipt());
|
|
261
|
+
assert.throws(() => close(required.project, "feature"), /explicit supersession/);
|
|
262
|
+
importExternal(required.project, "feature", required.receipt({ supersedes: [json(path.join(required.project, blockedReceipt)).id], supersessionReason: "External reviewer verified the prior issue is resolved; fixture source report" }));
|
|
263
|
+
assert.equal(close(required.project, "feature").phase, "DONE");
|
|
264
|
+
assert.ok(check(required.project, required.task(), "done").externalReview.findings[0].supersededBy);
|
|
265
|
+
} finally { off.dispose(); advisory.dispose(); required.dispose(); }
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("external receipts reject stale contracts, stale snapshots and duplicate IDs with different content", () => {
|
|
269
|
+
const f = fixture({ external: "required" });
|
|
270
|
+
try {
|
|
271
|
+
const stale = f.receipt();
|
|
272
|
+
reclassify(f.project, "feature", { risk: "CONTROLLED", reason: "Additional risk found", evidence: ["fixture inspection source"] });
|
|
273
|
+
assert.throws(() => importExternal(f.project, "feature", stale), /stale/);
|
|
274
|
+
assert.throws(() => importExternal(f.project, "feature", f.receipt({ snapshot: "different" })), /stale/);
|
|
275
|
+
assert.throws(() => importExternal(f.project, "feature", f.receipt({ findings: [{}] })), /malformed findings/);
|
|
276
|
+
const current = f.receipt({ id: "external-123" });
|
|
277
|
+
importExternal(f.project, "feature", current);
|
|
278
|
+
importExternal(f.project, "feature", current);
|
|
279
|
+
assert.equal(f.task().externalReviews.length, 1);
|
|
280
|
+
assert.throws(() => importExternal(f.project, "feature", f.receipt({ id: "external-123", verdict: "BLOCKED" })), /different content/);
|
|
281
|
+
const old = process.env.CLAUDECODE;
|
|
282
|
+
try { process.env.CLAUDECODE = "1"; assert.throws(() => importExternal(f.project, "feature", current), /outside the Claude session/); }
|
|
283
|
+
finally { if (old === undefined) delete process.env.CLAUDECODE; else process.env.CLAUDECODE = old; }
|
|
284
|
+
} finally { f.dispose(); }
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("partial reports and missing required closure artifacts never unlock DONE", () => {
|
|
288
|
+
const f = fixture({ contract: { requiredArtifacts: [".nccgs/evidence/build.txt"] } });
|
|
289
|
+
try {
|
|
290
|
+
f.work(); f.review(); f.report("partial");
|
|
291
|
+
assert.throws(() => close(f.project, "feature"), /not success/);
|
|
292
|
+
f.report("success");
|
|
293
|
+
assert.throws(() => close(f.project, "feature"), /closure artifact missing/);
|
|
294
|
+
f.report("blocked");
|
|
295
|
+
f.report("success", { artifacts: [f.write(".nccgs/evidence/build.txt", "fixture build artifact")] });
|
|
296
|
+
assert.equal(close(f.project, "feature").phase, "DONE");
|
|
297
|
+
f.write(".nccgs/evidence/build.txt", "changed artifact");
|
|
298
|
+
assert.throws(() => close(f.project, "feature"), /stale artifact/);
|
|
299
|
+
} finally { f.dispose(); }
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Explicit observed-effort boundaries, using synthetic correlated provider-log fixtures.
|
|
303
|
+
// These cases never establish actual native model/effort execution.
|
|
304
|
+
test("CONTROLLED accepts observed xhigh and its independent Fable high review", () => {
|
|
305
|
+
const f = fixture({ strictEffort: true, contract: { objective: "Migrate the save serialization format", scope: "local" } });
|
|
306
|
+
try {
|
|
307
|
+
assert.equal(f.task().risk, "CONTROLLED");
|
|
308
|
+
assert.equal(f.task().implementationRoute.effort, "xhigh");
|
|
309
|
+
f.work();
|
|
310
|
+
const work = json(path.join(f.project, f.task().stages.implementation.runtime.path));
|
|
311
|
+
assert.equal(work.observedEffort, "xhigh");
|
|
312
|
+
f.review();
|
|
313
|
+
const review = json(path.join(f.project, f.task().stages.review.runtime.path));
|
|
314
|
+
assert.equal(review.observedModel, "claude-fable-5-1");
|
|
315
|
+
assert.equal(f.task().execution.profile.models.review.effort, "high");
|
|
316
|
+
assert.equal(review.observedEffort, "high");
|
|
317
|
+
} finally { f.dispose(); }
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("CONTROLLED rejects observed high when xhigh was selected and retains its dispatch", () => {
|
|
321
|
+
const f = fixture({ strictEffort: true, contract: { objective: "Migrate the save serialization format", scope: "local" } });
|
|
322
|
+
try {
|
|
323
|
+
assert.throws(() => f.work({}, { observedEffort: "high" }), /Observed effort does not match selected route/);
|
|
324
|
+
assert.equal(f.task().stages.implementation, undefined);
|
|
325
|
+
assert.equal(status(f.project, "feature").usage.dispatches, 1);
|
|
326
|
+
} finally { f.dispose(); }
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("CONTROLLED does not infer observed xhigh from its configured route", () => {
|
|
330
|
+
const f = fixture({ strictEffort: true, contract: { objective: "Migrate the save serialization format", scope: "local" } });
|
|
331
|
+
try {
|
|
332
|
+
assert.throws(() => f.work({}, { observedEffort: null }), /effort is UNKNOWN/);
|
|
333
|
+
assert.equal(f.task().stages.implementation, undefined);
|
|
334
|
+
assert.equal(status(f.project, "feature").usage.dispatches, 1);
|
|
335
|
+
} finally { f.dispose(); }
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("Fable review rejects observed xhigh when high was selected without approving the task", () => {
|
|
339
|
+
const f = fixture({ strictEffort: true, contract: { objective: "Migrate the save serialization format", scope: "local" } });
|
|
340
|
+
try {
|
|
341
|
+
f.work();
|
|
342
|
+
assert.throws(() => f.review({}, { observedEffort: "xhigh" }), /Observed effort does not match selected route/);
|
|
343
|
+
assert.equal(f.task().stages.review, undefined); assert.equal(f.task().reviewBudget.used, 0);
|
|
344
|
+
assert.equal(status(f.project, "feature").usage.dispatches, 2);
|
|
345
|
+
} finally { f.dispose(); }
|
|
346
|
+
});
|