nccgs 1.1.0 → 1.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/nccgs/VERSION +1 -1
- package/.claude/nccgs/tools/approve-overrides.mjs +81 -0
- package/.claude/nccgs/tools/configure-models.mjs +5 -0
- package/.claude/nccgs/tools/integrity.mjs +69 -0
- package/.claude/nccgs/workflow-catalog.json +1 -1
- package/.claude/skills/migrate-project/SKILL.md +3 -1
- package/.claude/skills/migrate-project/references/procedure.md +13 -1
- package/.claude/skills/{code-review → nccgs-code-review}/SKILL.md +1 -2
- package/README.md +9 -5
- package/UPGRADING.md +17 -4
- package/VERSION +1 -1
- package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +13 -6
- package/docs/WORKFLOWS.md +1 -1
- package/package-assets/nccgs-project.gitignore +2 -0
- package/package.json +1 -1
- package/scripts/cli.mjs +1 -1
- package/scripts/doctor.mjs +75 -17
- package/scripts/install.mjs +218 -86
- package/scripts/validate.mjs +17 -2
- package/tests/framework.test.mjs +70 -6
package/.claude/nccgs/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.1.
|
|
1
|
+
1.1.1
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { fingerprintFile } from "./integrity.mjs";
|
|
7
|
+
|
|
8
|
+
const ownDir = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const projectArg = args.indexOf("--project");
|
|
11
|
+
const project = path.resolve(projectArg >= 0 ? args[projectArg + 1] : path.resolve(ownDir, "../../../.."));
|
|
12
|
+
const approvePending = args.includes("--approve-pending");
|
|
13
|
+
const manifestPath = path.join(project, ".nccgs", "framework-overrides.json");
|
|
14
|
+
|
|
15
|
+
if (projectArg >= 0 && !args[projectArg + 1]) {
|
|
16
|
+
console.error("--project requires a directory path.");
|
|
17
|
+
process.exit(2);
|
|
18
|
+
}
|
|
19
|
+
if (!fs.existsSync(manifestPath)) {
|
|
20
|
+
console.log("No NCCGS framework overrides are recorded.");
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let manifest;
|
|
25
|
+
try { manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); }
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.error(`Invalid framework override manifest: ${error.message}`);
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
if (manifest.schemaVersion !== 1) {
|
|
31
|
+
console.error(`Unsupported framework override schema: ${manifest.schemaVersion ?? "unknown"}`);
|
|
32
|
+
process.exit(2);
|
|
33
|
+
}
|
|
34
|
+
for (const item of manifest.overrides ?? []) {
|
|
35
|
+
if (!isManagedRelative(item.path) || !new Set(["preserve", "absent"]).has(item.disposition)
|
|
36
|
+
|| !new Set(["approved", "pending_review"]).has(item.status)) {
|
|
37
|
+
console.error(`Invalid framework override record: ${item.path ?? "path missing"}`);
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const pending = (manifest.overrides ?? []).filter((item) => item.status !== "approved");
|
|
43
|
+
if (!approvePending) {
|
|
44
|
+
if (!pending.length) console.log("All NCCGS framework overrides are approved.");
|
|
45
|
+
else {
|
|
46
|
+
console.log(`${pending.length} NCCGS framework override(s) await migration review:`);
|
|
47
|
+
for (const item of pending) console.log(`- ${item.path} (${item.disposition})`);
|
|
48
|
+
console.log("After reviewing/rebasing every item, run again with --approve-pending.");
|
|
49
|
+
}
|
|
50
|
+
process.exit(pending.length ? 1 : 0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const item of pending) {
|
|
54
|
+
const file = path.join(project, ...item.path.split("/"));
|
|
55
|
+
if (item.disposition === "absent") {
|
|
56
|
+
if (fs.existsSync(file)) {
|
|
57
|
+
console.error(`Cannot approve absent override because the file exists: ${item.path}`);
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
item.sha256 = null;
|
|
61
|
+
item.canonicalSha256 = null;
|
|
62
|
+
} else {
|
|
63
|
+
if (!fs.existsSync(file)) {
|
|
64
|
+
console.error(`Cannot approve preserved override because the file is missing: ${item.path}`);
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
67
|
+
Object.assign(item, fingerprintFile(file));
|
|
68
|
+
}
|
|
69
|
+
item.status = "approved";
|
|
70
|
+
item.reviewedAt = new Date().toISOString();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
manifest.updatedAt = new Date().toISOString();
|
|
74
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
75
|
+
console.log(`Approved ${pending.length} NCCGS framework override(s) after migration review.`);
|
|
76
|
+
|
|
77
|
+
function isManagedRelative(value) {
|
|
78
|
+
if (typeof value !== "string" || !value.startsWith(".claude/")) return false;
|
|
79
|
+
const parts = value.split("/");
|
|
80
|
+
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
81
|
+
}
|
|
@@ -6,6 +6,10 @@ const ownDir = path.dirname(fileURLToPath(import.meta.url));
|
|
|
6
6
|
const args = process.argv.slice(2);
|
|
7
7
|
const projectArg = args.indexOf("--project");
|
|
8
8
|
const profileArg = args.indexOf("--profile");
|
|
9
|
+
const skippedPaths = new Set();
|
|
10
|
+
for (let index = 0; index < args.length; index++) {
|
|
11
|
+
if (args[index] === "--skip-path" && args[index + 1]) skippedPaths.add(args[index + 1].replace(/\\/g, "/"));
|
|
12
|
+
}
|
|
9
13
|
const project = path.resolve(projectArg >= 0 ? args[projectArg + 1] : path.resolve(ownDir, "../../../.."));
|
|
10
14
|
const catalogPath = path.join(project, ".claude", "nccgs", "studio.json");
|
|
11
15
|
const catalog = JSON.parse(fs.readFileSync(catalogPath, "utf8"));
|
|
@@ -19,6 +23,7 @@ if (!profile) throw new Error(`Unknown model profile '${profileName}'. Expected:
|
|
|
19
23
|
let changed = 0;
|
|
20
24
|
for (const agent of catalog.agents) {
|
|
21
25
|
const file = path.join(project, ".claude", "agents", `${agent.name}.md`);
|
|
26
|
+
if (skippedPaths.has(`.claude/agents/${agent.name}.md`)) continue;
|
|
22
27
|
if (!fs.existsSync(file)) continue;
|
|
23
28
|
let text = fs.readFileSync(file, "utf8");
|
|
24
29
|
const model = profile[agent.modelClass];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
|
|
4
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
5
|
+
|
|
6
|
+
export function sha256(value) {
|
|
7
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function textContent(value) {
|
|
11
|
+
const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
12
|
+
if (buffer.includes(0)) return null;
|
|
13
|
+
try { return utf8.decode(buffer); }
|
|
14
|
+
catch { return null; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeLf(value) {
|
|
18
|
+
const text = textContent(value);
|
|
19
|
+
if (text === null) return null;
|
|
20
|
+
return `${text.replace(/\r\n?/g, "\n").replace(/\n*$/, "")}\n`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function fingerprintBuffer(value) {
|
|
24
|
+
const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
25
|
+
const normalized = normalizeLf(buffer);
|
|
26
|
+
return {
|
|
27
|
+
sha256: sha256(buffer),
|
|
28
|
+
...(normalized === null ? {} : { canonicalSha256: sha256(Buffer.from(normalized, "utf8")) })
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function fingerprintFile(file) {
|
|
33
|
+
return fingerprintBuffer(fs.readFileSync(file));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function equivalentBuffers(left, right) {
|
|
37
|
+
const a = fingerprintBuffer(left);
|
|
38
|
+
const b = fingerprintBuffer(right);
|
|
39
|
+
if (a.sha256 === b.sha256) return true;
|
|
40
|
+
return Boolean(a.canonicalSha256 && a.canonicalSha256 === b.canonicalSha256);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function equivalentFiles(left, right) {
|
|
44
|
+
return equivalentBuffers(fs.readFileSync(left), fs.readFileSync(right));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function matchesFingerprint(file, expected) {
|
|
48
|
+
if (!expected || !fs.existsSync(file)) return false;
|
|
49
|
+
const buffer = fs.readFileSync(file);
|
|
50
|
+
const actual = fingerprintBuffer(buffer);
|
|
51
|
+
if (expected.sha256 && actual.sha256 === expected.sha256) return true;
|
|
52
|
+
if (expected.canonicalSha256 && actual.canonicalSha256 === expected.canonicalSha256) return true;
|
|
53
|
+
|
|
54
|
+
// NCCGS 1.1.0 stored only a raw hash. Accept the two normal Git text
|
|
55
|
+
// representations so a checkout's LF policy does not manufacture drift.
|
|
56
|
+
if (expected.sha256 && actual.canonicalSha256) {
|
|
57
|
+
const lf = normalizeLf(buffer);
|
|
58
|
+
if (sha256(Buffer.from(lf, "utf8")) === expected.sha256) return true;
|
|
59
|
+
const crlf = lf.replace(/\n/g, "\r\n");
|
|
60
|
+
if (sha256(Buffer.from(crlf, "utf8")) === expected.sha256) return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function sameFingerprint(left, right) {
|
|
66
|
+
if (!left || !right) return false;
|
|
67
|
+
if (left.sha256 && right.sha256 && left.sha256 === right.sha256) return true;
|
|
68
|
+
return Boolean(left.canonicalSha256 && right.canonicalSha256 && left.canonicalSha256 === right.canonicalSha256);
|
|
69
|
+
}
|
|
@@ -8,6 +8,8 @@ allowed-tools: Read, Glob, Grep, Write, Edit, Bash, Task, AskUserQuestion
|
|
|
8
8
|
|
|
9
9
|
Treat brownfield adoption and migrations explicitly requested by the installer as CONTROLLED work. Read the complete [migration procedure](references/procedure.md). Default to `full` when no mode is given. Do not require migration merely because NCCGS was updated when the installer reported `No project migration required`.
|
|
10
10
|
|
|
11
|
+
When `.nccgs/framework-overrides.json` exists, treat every `pending_review` entry as preserved project truth, not installer debris. Compare the project version with the current NCCGS version, keep or rebase the customization deliberately, and record the decision in the migration plan. Only after every pending item has been reviewed and the final bytes or intentional absence are correct, run `node .claude/nccgs/tools/approve-overrides.mjs --project . --approve-pending`. Never approve first to make `doctor` green.
|
|
12
|
+
|
|
11
13
|
## Modes
|
|
12
14
|
|
|
13
15
|
- `full` (default): run review, plan, apply, and verify in one invocation. Continue across phase boundaries without asking for ceremonial approval. Pause only for an unresolved Product Owner decision, destructive action, external side effect, or authority outside the current request.
|
|
@@ -16,7 +18,7 @@ Treat brownfield adoption and migrations explicitly requested by the installer a
|
|
|
16
18
|
- `apply`: execute an approved plan in validated batches with backups.
|
|
17
19
|
- `verify`: prove policy, routing, hooks, Unity integration, evidence, closure, and a representative work path.
|
|
18
20
|
|
|
19
|
-
Migration decisions must cover canonical sources, ownership/protected paths, architecture, Unity tooling, model profile/fallback, enabled departments, context packets, verification baselines, closure safeguards, Git tracking, local settings, obsolete global skills, and rollback.
|
|
21
|
+
Migration decisions must cover canonical sources, ownership/protected paths, architecture, Unity tooling, model profile/fallback, enabled departments, context packets, verification baselines, closure safeguards, Git tracking, local settings, preserved framework overrides, obsolete global skills, and rollback.
|
|
20
22
|
|
|
21
23
|
In `full` mode, write the plan before applying it, preserve the same evidence and rollback guarantees as the individual modes, and stop safely if review finds a decision that would materially change project truth. A reversible plan created within the same invocation is approved for in-scope application unless project policy explicitly requires a separate approval.
|
|
22
24
|
|
|
@@ -28,6 +28,10 @@ requires no project migration, report that fact and do not manufacture migration
|
|
|
28
28
|
strongest active safeguard until the PO explicitly accepts a weaker model.
|
|
29
29
|
7. Determine the project model profile, enabled departments, permitted fallback,
|
|
30
30
|
orchestration depth/parallelism, context-packet locations, and hook compatibility.
|
|
31
|
+
8. If `.nccgs/framework-overrides.json` exists, inspect every override. Diff its
|
|
32
|
+
preserved bytes or intentional absence against the installed upstream version,
|
|
33
|
+
and distinguish a still-needed project policy from a customization now supplied
|
|
34
|
+
by NCCGS. Line-ending-only differences are not overrides.
|
|
31
35
|
|
|
32
36
|
## Plan
|
|
33
37
|
|
|
@@ -42,6 +46,7 @@ Create `.nccgs/migrations/<date>-migration-plan.md` with:
|
|
|
42
46
|
- ordered reversible batches and dependencies;
|
|
43
47
|
- exact broad/irreversible decisions requiring PO authority;
|
|
44
48
|
- backup/rollback method;
|
|
49
|
+
- an explicit KEEP/REBASE/RETIRE decision for every pending framework override;
|
|
45
50
|
- validation for each batch and final end-to-end proof.
|
|
46
51
|
|
|
47
52
|
Prioritize authority and policy conflicts, then model/routing conflicts, Unity
|
|
@@ -64,6 +69,12 @@ traceability/evidence, lifecycle hooks, workflow replacement, and cleanup.
|
|
|
64
69
|
- Do not leave two active agents/skills advertising mutually exclusive Unity
|
|
65
70
|
execution bridges. Disable obsolete global routing signals outside the repository
|
|
66
71
|
through an explicit user action.
|
|
72
|
+
- Preserve pending framework overrides while reviewing them. Rebase each retained
|
|
73
|
+
override onto the newly installed upstream content when needed. After all entries
|
|
74
|
+
match the migration decisions, run
|
|
75
|
+
`node .claude/nccgs/tools/approve-overrides.mjs --project . --approve-pending`.
|
|
76
|
+
Approval records the final hashes; it is the last apply step, never a substitute
|
|
77
|
+
for the review.
|
|
67
78
|
|
|
68
79
|
## Verify
|
|
69
80
|
|
|
@@ -72,4 +83,5 @@ role/workflow catalogs, bounded delegation, hook health, context packet freshnes
|
|
|
72
83
|
canonical map, requirements/state, Unity Skills integrity, CLI/Pipeline operation,
|
|
73
84
|
relevant tests, runtime evidence, and subjective-gate routing. Verify that required
|
|
74
85
|
linked artifacts block DONE in `linked` or `strict` mode. Use a small representative
|
|
75
|
-
task or dry run when product changes are not authorized.
|
|
86
|
+
task or dry run when product changes are not authorized. Run `npx nccgs@latest doctor`
|
|
87
|
+
and require zero pending framework overrides before declaring migration complete.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: code-review
|
|
2
|
+
name: nccgs-code-review
|
|
3
3
|
description: Performs a read-only code review against requirements, architecture, Unity constraints, ownership, tests, performance, security, and regression risk.
|
|
4
4
|
allowed-tools: Read, Glob, Grep, Bash, Task
|
|
5
5
|
---
|
|
@@ -11,4 +11,3 @@ Read `.nccgs/project.yaml`, `.nccgs/state.md`, and the active context packet whe
|
|
|
11
11
|
Read the context packet, final diff, governing decisions, affected callers, and tests. Route distinct Unity, networking, security, performance, or UI lenses when needed. Report only actionable findings with severity, exact evidence, impact, and remediation direction. Verify that tests assert requirements and that serialized/lifecycle behavior is addressed. Do not edit or praise generally; state when no material issue is found.
|
|
12
12
|
|
|
13
13
|
Return outcome, evidence, affected records, blockers or decisions, residual risk, and the exact next workflow. Do not claim DONE unless this is the `closure` skill and every configured gate passes.
|
|
14
|
-
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ specialization of a full game-development team with project-aware governance,
|
|
|
5
5
|
bounded orchestration, configurable model routing, official Unity Skills, objective
|
|
6
6
|
evidence, and strict closure.
|
|
7
7
|
|
|
8
|
-
Version 1.1 contains:
|
|
8
|
+
Version 1.1.1 contains:
|
|
9
9
|
|
|
10
10
|
- 45 studio agents across leadership, design, programming, Unity, content, quality,
|
|
11
11
|
and release;
|
|
@@ -56,9 +56,12 @@ npx nccgs@latest doctor
|
|
|
56
56
|
The current directory is the installation target. `--project <directory>` remains
|
|
57
57
|
available for automation, but is not needed for ordinary use.
|
|
58
58
|
|
|
59
|
-
Updates are hash-aware. Files that still match the previous
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
Updates are hash-aware and line-ending-aware. Files that still match the previous
|
|
60
|
+
install state update automatically even when Git converted CRLF to LF. Genuine
|
|
61
|
+
project modifications are preserved, recorded in `.nccgs/framework-overrides.json`,
|
|
62
|
+
and routed through one `/migrate-project` review instead of blocking the whole
|
|
63
|
+
framework update. `--force` remains an explicit discard-and-replace escape hatch;
|
|
64
|
+
it is not the normal upgrade path.
|
|
62
65
|
|
|
63
66
|
The installer validates the package, backs up replaced framework/settings files to
|
|
64
67
|
`.nccgs/backups/<timestamp>/`, copies `.claude/`, initializes missing `.nccgs/`
|
|
@@ -113,7 +116,7 @@ node .\.claude\nccgs\tools\configure-models.mjs --project . --profile inherit
|
|
|
113
116
|
| `/status` | Current work, roles/models, evidence, blockers, closure, and next action |
|
|
114
117
|
|
|
115
118
|
Thirty-six discoverable workflows include focused modules for architecture decisions,
|
|
116
|
-
context packets, story readiness, code/design/evidence review, closure, QA planning,
|
|
119
|
+
context packets, story readiness, NCCGS code/design/evidence review, closure, QA planning,
|
|
117
120
|
triage, performance, security, accessibility, balance, prototypes, playtests,
|
|
118
121
|
localization, hotfixes, milestones, assets, compatibility, incidents, and UI review.
|
|
119
122
|
Ordinary natural-language requests still work; the user does not need to manually
|
|
@@ -124,6 +127,7 @@ chain these modules.
|
|
|
124
127
|
```text
|
|
125
128
|
.nccgs/
|
|
126
129
|
project.yaml authority, model, studio, context, ownership and gate policy
|
|
130
|
+
framework-overrides.json reviewed project-specific framework customizations
|
|
127
131
|
state.md compact recovery checkpoint
|
|
128
132
|
requirements.yaml targeted requirement registry
|
|
129
133
|
context/ task-specific source and ownership maps
|
package/UPGRADING.md
CHANGED
|
@@ -9,10 +9,23 @@ npx nccgs@latest update
|
|
|
9
9
|
npx nccgs@latest doctor
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
Version 1.1
|
|
13
|
-
previous install state update automatically
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
Version 1.1.1 makes updates hash-aware, line-ending-aware, and override-aware.
|
|
13
|
+
Managed files that still match the previous install state update automatically;
|
|
14
|
+
CRLF/LF conversion alone is accepted as the same text. Genuine project-modified or
|
|
15
|
+
intentionally missing framework files are preserved and recorded in
|
|
16
|
+
`.nccgs/framework-overrides.json` as `pending_review`. The rest of the framework is
|
|
17
|
+
updated normally, so `--force` is no longer required to get past intentional local
|
|
18
|
+
customizations. Existing `.nccgs/project.yaml` remains project-owned and is never
|
|
19
|
+
blindly overwritten.
|
|
20
|
+
|
|
21
|
+
When pending overrides are reported, open a fresh Claude Code session and run
|
|
22
|
+
`/migrate-project` once. The workflow reviews and rebases each override, migrates the
|
|
23
|
+
project policy when needed, and records approval of the final hashes. A direct
|
|
24
|
+
`doctor` remains red until pending override review is complete. Use `--force` only
|
|
25
|
+
when you intentionally want to back up and discard every detected customization.
|
|
26
|
+
|
|
27
|
+
NCCGS 1.1.1 also names its specialized review command `/nccgs-code-review` to avoid
|
|
28
|
+
colliding with Claude Code's built-in `/code-review` command.
|
|
16
29
|
|
|
17
30
|
When the installer reports that project migration is required, open a new Claude
|
|
18
31
|
Code session and run one command:
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.1.
|
|
1
|
+
1.1.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Hướng dẫn migrate và sử dụng NCCGS 1.1
|
|
1
|
+
# Hướng dẫn migrate và sử dụng NCCGS 1.1.1
|
|
2
2
|
|
|
3
|
-
NCCGS 1.1 là studio operating system dành cho Unity project trong Claude Code. Nó
|
|
3
|
+
NCCGS 1.1.1 là studio operating system dành cho Unity project trong Claude Code. Nó
|
|
4
4
|
không thay thế cách chat trực tiếp; nó tự phân loại rủi ro, chọn workflow, agent và
|
|
5
5
|
model phù hợp, sử dụng official Unity Skills, thu thập bằng chứng và áp dụng closure.
|
|
6
6
|
|
|
@@ -46,9 +46,12 @@ npx nccgs@latest doctor
|
|
|
46
46
|
NCCGS mặc định cài vào directory hiện tại. Bạn không cần truyền đường dẫn project;
|
|
47
47
|
hãy mở PowerShell tại Unity project root hoặc dùng `cd` đến đó trước.
|
|
48
48
|
|
|
49
|
-
Từ 1.1, update kiểm tra
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
Từ 1.1.1, update kiểm tra cả raw hash và hash text đã chuẩn hóa line ending. Việc Git
|
|
50
|
+
đổi CRLF/LF không còn bị báo nhầm là sửa file. File thật sự đã customize hoặc cố ý
|
|
51
|
+
xóa sẽ được giữ nguyên, ghi vào `.nccgs/framework-overrides.json` với trạng thái
|
|
52
|
+
`pending_review`, còn các file an toàn khác vẫn cập nhật bình thường. Sau đó chỉ cần
|
|
53
|
+
chạy `/migrate-project` một lần để review/rebase và phê duyệt các override. Không cần
|
|
54
|
+
`--force`; chỉ dùng cờ đó khi thực sự muốn backup rồi loại bỏ customization.
|
|
52
55
|
|
|
53
56
|
Installer sẽ:
|
|
54
57
|
|
|
@@ -60,6 +63,10 @@ Installer sẽ:
|
|
|
60
63
|
6. cấu hình model cho agents;
|
|
61
64
|
7. thêm NCCGS import vào `CLAUDE.md`.
|
|
62
65
|
|
|
66
|
+
Nếu installer báo `Project migration required`, mở Claude Code session mới và chạy
|
|
67
|
+
`/migrate-project`. `doctor` trực tiếp sẽ tiếp tục báo pending cho tới khi workflow
|
|
68
|
+
đã review xong mọi override; đây là safety gate có chủ đích.
|
|
69
|
+
|
|
63
70
|
Sau khi cài, đóng phiên Claude Code cũ và mở phiên mới tại project root.
|
|
64
71
|
|
|
65
72
|
## 3. Chọn model profile
|
|
@@ -185,7 +192,7 @@ NCCGS sẽ tự chọn:
|
|
|
185
192
|
| `/release` | Tạo release candidate |
|
|
186
193
|
| `/status` | Xem trạng thái, model, agents, evidence và blockers |
|
|
187
194
|
|
|
188
|
-
Các workflow chuyên biệt như `/architecture-decision`, `/code-review`,
|
|
195
|
+
Các workflow chuyên biệt như `/architecture-decision`, `/nccgs-code-review`,
|
|
189
196
|
`/performance-audit`, `/security-audit`, `/balance-review`, `/playtest`, `/hotfix`,
|
|
190
197
|
`/incident-recovery` có thể gọi trực tiếp, nhưng thông thường `/work` tự phối hợp.
|
|
191
198
|
|
package/docs/WORKFLOWS.md
CHANGED
|
@@ -33,7 +33,7 @@ available when a user wants a bounded workflow or output.
|
|
|
33
33
|
- **Design**: `design`, `design-review`, `balance-review`, `accessibility-review`,
|
|
34
34
|
`ui-review`, `prototype-feature`, `playtest`.
|
|
35
35
|
- **Technical**: `architecture-decision`, `dependency-review`,
|
|
36
|
-
`compatibility-review`, `code-review`, `performance-audit`, `security-audit`.
|
|
36
|
+
`compatibility-review`, `nccgs-code-review`, `performance-audit`, `security-audit`.
|
|
37
37
|
- **Implementation/quality**: `work`, `qa-plan`, `test`, `bug-triage`,
|
|
38
38
|
`evidence-review`, `review`, `asset-audit`, `closure`.
|
|
39
39
|
- **Delivery/recovery**: `release-readiness`, `release`, `hotfix`,
|
package/package.json
CHANGED
package/scripts/cli.mjs
CHANGED
|
@@ -53,7 +53,7 @@ Install target:
|
|
|
53
53
|
|
|
54
54
|
Options:
|
|
55
55
|
--dry-run Preview without writing files
|
|
56
|
-
--force Back up and
|
|
56
|
+
--force Back up and discard detected project overrides
|
|
57
57
|
--model-profile <profile> quality, balanced, or inherit
|
|
58
58
|
--project <directory> Optional explicit target directory
|
|
59
59
|
--allow-non-unity Allow a target without Unity markers`);
|
package/scripts/doctor.mjs
CHANGED
|
@@ -1,39 +1,70 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import crypto from "node:crypto";
|
|
4
3
|
import fs from "node:fs";
|
|
5
4
|
import path from "node:path";
|
|
5
|
+
import { matchesFingerprint, sameFingerprint } from "../.claude/nccgs/tools/integrity.mjs";
|
|
6
6
|
|
|
7
7
|
const args = process.argv.slice(2);
|
|
8
8
|
const projectIndex = args.indexOf("--project");
|
|
9
|
-
const
|
|
9
|
+
const allowMigrationRequired = args.includes("--allow-migration-required") || args.includes("--allow-policy-migration");
|
|
10
10
|
const warnings = [];
|
|
11
|
+
const errors = [];
|
|
11
12
|
if (projectIndex >= 0 && !args[projectIndex + 1]) {
|
|
12
13
|
console.error("--project requires a directory path.");
|
|
13
14
|
process.exit(2);
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
const project = projectIndex >= 0 ? path.resolve(args[projectIndex + 1]) : process.cwd();
|
|
17
|
-
const errors = [];
|
|
18
18
|
for (const marker of ["Assets", "Packages", "ProjectSettings"]) {
|
|
19
19
|
if (!fs.existsSync(path.join(project, marker))) errors.push(`Missing Unity marker: ${marker}`);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const statePath = path.join(project, ".claude", "nccgs", "install-state.json");
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
const overridePath = path.join(project, ".nccgs", "framework-overrides.json");
|
|
24
|
+
const state = readJson(statePath, "NCCGS install state", true);
|
|
25
|
+
const overrideManifest = readJson(overridePath, "NCCGS framework override manifest", false);
|
|
26
|
+
if (state && ![1, 2, 3].includes(state.schemaVersion)) errors.push(`Unsupported install-state schema: ${state.schemaVersion ?? "unknown"}.`);
|
|
27
|
+
if (overrideManifest && overrideManifest.schemaVersion !== 1) errors.push(`Unsupported framework override schema: ${overrideManifest.schemaVersion ?? "unknown"}.`);
|
|
28
|
+
|
|
29
|
+
const overrides = new Map();
|
|
30
|
+
for (const item of overrideManifest?.overrides ?? []) {
|
|
31
|
+
if (!isManagedRelative(item.path) || !new Set(["preserve", "absent"]).has(item.disposition)
|
|
32
|
+
|| !new Set(["approved", "pending_review"]).has(item.status)) {
|
|
33
|
+
errors.push(`Invalid framework override record: ${item.path ?? "path missing"}`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (overrides.has(item.path)) errors.push(`Duplicate framework override: ${item.path}`);
|
|
37
|
+
overrides.set(item.path, item);
|
|
28
38
|
}
|
|
29
39
|
|
|
40
|
+
let verifiedManaged = 0;
|
|
30
41
|
if (state) {
|
|
31
42
|
for (const item of state.files ?? []) {
|
|
43
|
+
if (!isManagedRelative(item.relative)) {
|
|
44
|
+
errors.push(`Unsafe managed path in install state: ${item.relative ?? "missing"}`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
32
47
|
const file = path.join(project, ...item.relative.split("/"));
|
|
48
|
+
const override = overrides.get(item.relative);
|
|
49
|
+
if (override) {
|
|
50
|
+
const recordedUpstream = {
|
|
51
|
+
sha256: override.upstreamSha256,
|
|
52
|
+
canonicalSha256: override.upstreamCanonicalSha256
|
|
53
|
+
};
|
|
54
|
+
if (!sameFingerprint(item, recordedUpstream)) errors.push(`Framework override has stale upstream hashes: ${item.relative}`);
|
|
55
|
+
validateOverride(override, file);
|
|
56
|
+
verifiedManaged++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
33
59
|
if (!fs.existsSync(file)) errors.push(`Managed file is missing: ${item.relative}`);
|
|
34
|
-
else if (
|
|
60
|
+
else if (!matchesFingerprint(file, item)) errors.push(`Managed file was modified: ${item.relative}`);
|
|
61
|
+
else verifiedManaged++;
|
|
35
62
|
}
|
|
36
63
|
}
|
|
64
|
+
for (const item of overrides.values()) {
|
|
65
|
+
if ((state?.files ?? []).some((managed) => managed.relative === item.path)) continue;
|
|
66
|
+
validateOverride(item, path.join(project, ...item.path.split("/")));
|
|
67
|
+
}
|
|
37
68
|
|
|
38
69
|
const claudePath = path.join(project, "CLAUDE.md");
|
|
39
70
|
if (!fs.existsSync(claudePath) || !fs.readFileSync(claudePath, "utf8").includes("@.claude/nccgs/constitution.md")) {
|
|
@@ -46,15 +77,12 @@ if (!fs.existsSync(policyPath)) errors.push(".nccgs/project.yaml is missing.");
|
|
|
46
77
|
else {
|
|
47
78
|
const match = fs.readFileSync(policyPath, "utf8").match(/^schema_version:\s*(\d+)\s*$/m);
|
|
48
79
|
policySchema = match ? Number(match[1]) : null;
|
|
49
|
-
if (policySchema !== 2) {
|
|
50
|
-
const message = `Project policy schema must be 2; found ${policySchema ?? "unknown"}.`;
|
|
51
|
-
if (allowPolicyMigration) warnings.push(message);
|
|
52
|
-
else errors.push(message);
|
|
53
|
-
}
|
|
80
|
+
if (policySchema !== 2) migrationIssue(`Project policy schema must be 2; found ${policySchema ?? "unknown"}.`);
|
|
54
81
|
}
|
|
55
82
|
|
|
56
83
|
if (errors.length) {
|
|
57
84
|
for (const error of errors) console.error(`ERROR: ${error}`);
|
|
85
|
+
for (const warning of warnings) console.warn(`WARN: ${warning}`);
|
|
58
86
|
console.error(`NCCGS doctor failed with ${errors.length} error(s).`);
|
|
59
87
|
process.exit(1);
|
|
60
88
|
}
|
|
@@ -63,9 +91,39 @@ for (const warning of warnings) console.warn(`WARN: ${warning}`);
|
|
|
63
91
|
console.log("NCCGS doctor passed.");
|
|
64
92
|
console.log(`Framework version: ${state.frameworkVersion}`);
|
|
65
93
|
console.log(`Model profile: ${state.modelProfile}`);
|
|
66
|
-
console.log(`Managed files verified: ${
|
|
94
|
+
console.log(`Managed files verified: ${verifiedManaged}`);
|
|
95
|
+
console.log(`Framework overrides: ${overrides.size} (${[...overrides.values()].filter((item) => item.status !== "approved").length} pending review)`);
|
|
67
96
|
console.log(`Project policy schema: ${policySchema ?? "migration required"}`);
|
|
68
97
|
|
|
69
|
-
function
|
|
70
|
-
|
|
98
|
+
function validateOverride(item, file) {
|
|
99
|
+
if (item.status !== "approved") migrationIssue(`Framework override awaits /migrate-project review: ${item.path}`);
|
|
100
|
+
if (item.disposition === "absent") {
|
|
101
|
+
if (fs.existsSync(file)) errors.push(`Override expects a missing file, but it exists: ${item.path}`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!fs.existsSync(file)) errors.push(`Preserved framework override is missing: ${item.path}`);
|
|
105
|
+
else if (!matchesFingerprint(file, item)) errors.push(`Preserved framework override changed after it was recorded: ${item.path}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function migrationIssue(message) {
|
|
109
|
+
if (allowMigrationRequired) warnings.push(message);
|
|
110
|
+
else errors.push(message);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readJson(file, label, required) {
|
|
114
|
+
if (!fs.existsSync(file)) {
|
|
115
|
+
if (required) errors.push(`${label} is missing.`);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
try { return JSON.parse(fs.readFileSync(file, "utf8")); }
|
|
119
|
+
catch (error) {
|
|
120
|
+
errors.push(`${label} is invalid JSON: ${error.message}`);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isManagedRelative(value) {
|
|
126
|
+
if (typeof value !== "string" || !value.startsWith(".claude/")) return false;
|
|
127
|
+
const parts = value.split("/");
|
|
128
|
+
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
71
129
|
}
|
package/scripts/install.mjs
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import crypto from "node:crypto";
|
|
3
2
|
import fs from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
equivalentFiles,
|
|
7
|
+
fingerprintFile,
|
|
8
|
+
matchesFingerprint,
|
|
9
|
+
sameFingerprint
|
|
10
|
+
} from "../.claude/nccgs/tools/integrity.mjs";
|
|
6
11
|
|
|
7
12
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
8
13
|
const root = path.resolve(scriptDir, "..");
|
|
@@ -52,74 +57,108 @@ if (validation.status !== 0) {
|
|
|
52
57
|
const frameworkSource = path.join(root, ".claude");
|
|
53
58
|
const frameworkFiles = walk(frameworkSource).map((file) => ({
|
|
54
59
|
source: file,
|
|
55
|
-
relative: path.relative(root, file),
|
|
60
|
+
relative: posix(path.relative(root, file)),
|
|
56
61
|
destination: path.join(project, path.relative(root, file)),
|
|
57
62
|
}));
|
|
58
63
|
const npmExcludedUnityFile = ".claude/skills/setup-vivox-voice-chat/evals/.gitignore";
|
|
59
|
-
if (!frameworkFiles.some((item) =>
|
|
64
|
+
if (!frameworkFiles.some((item) => item.relative === npmExcludedUnityFile)) {
|
|
60
65
|
const fallback = path.join(root, "package-assets", "setup-vivox-voice-chat-evals.gitignore");
|
|
61
66
|
if (fs.existsSync(fallback)) {
|
|
62
67
|
frameworkFiles.push({
|
|
63
68
|
source: fallback,
|
|
64
|
-
relative: npmExcludedUnityFile
|
|
69
|
+
relative: npmExcludedUnityFile,
|
|
65
70
|
destination: path.join(project, ...npmExcludedUnityFile.split("/"))
|
|
66
71
|
});
|
|
67
72
|
frameworkFiles.sort((a, b) => a.relative.localeCompare(b.relative));
|
|
68
73
|
}
|
|
69
74
|
}
|
|
75
|
+
|
|
70
76
|
const installStatePath = path.join(project, ".claude", "nccgs", "install-state.json");
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
catch (error) {
|
|
75
|
-
console.error(`Invalid NCCGS install state at ${installStatePath}: ${error.message}`);
|
|
76
|
-
process.exit(2);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
77
|
+
const overrideManifestPath = path.join(project, ".nccgs", "framework-overrides.json");
|
|
78
|
+
const previousInstallState = readJsonIfPresent(installStatePath, "NCCGS install state");
|
|
79
|
+
const previousOverrideManifest = readJsonIfPresent(overrideManifestPath, "NCCGS framework override manifest");
|
|
79
80
|
if (updateOnly && !previousInstallState) {
|
|
80
81
|
console.error("NCCGS is not installed in this project. Run `npx nccgs@latest install` first.");
|
|
81
82
|
process.exit(2);
|
|
82
83
|
}
|
|
84
|
+
if (previousOverrideManifest && previousOverrideManifest.schemaVersion !== 1) {
|
|
85
|
+
console.error(`Unsupported framework override schema: ${previousOverrideManifest.schemaVersion ?? "unknown"}`);
|
|
86
|
+
process.exit(2);
|
|
87
|
+
}
|
|
88
|
+
for (const item of previousInstallState?.files ?? []) {
|
|
89
|
+
if (!isManagedRelative(item.relative)) {
|
|
90
|
+
console.error(`Unsafe managed path in install state: ${item.relative ?? "missing"}`);
|
|
91
|
+
process.exit(2);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const item of previousOverrideManifest?.overrides ?? []) {
|
|
95
|
+
if (!isManagedRelative(item.path)) {
|
|
96
|
+
console.error(`Unsafe path in framework override manifest: ${item.path ?? "missing"}`);
|
|
97
|
+
process.exit(2);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
83
101
|
const previousFiles = new Map((previousInstallState?.files ?? []).map((item) => [item.relative, item]));
|
|
84
|
-
const
|
|
85
|
-
const
|
|
102
|
+
const previousOverrides = new Map((previousOverrideManifest?.overrides ?? []).map((item) => [item.path, item]));
|
|
103
|
+
const currentFrameworkNames = new Set(frameworkFiles.map((item) => item.relative));
|
|
104
|
+
const currentVersion = fs.readFileSync(path.join(root, "VERSION"), "utf8").trim();
|
|
105
|
+
|
|
106
|
+
const filePlans = frameworkFiles.map((item) => planFrameworkFile(item));
|
|
107
|
+
const stalePlans = (previousInstallState?.files ?? [])
|
|
86
108
|
.filter((item) => !currentFrameworkNames.has(item.relative))
|
|
87
|
-
.map((item) => (
|
|
109
|
+
.map((item) => planStaleFile(item))
|
|
110
|
+
.filter(Boolean);
|
|
111
|
+
|
|
112
|
+
const activeOverridePaths = new Set([
|
|
113
|
+
...filePlans.filter((item) => item.override).map((item) => item.relative),
|
|
114
|
+
...stalePlans.filter((item) => item.override).map((item) => item.relative)
|
|
115
|
+
]);
|
|
116
|
+
const carriedOverrides = [];
|
|
117
|
+
for (const item of previousOverrides.values()) {
|
|
118
|
+
if (activeOverridePaths.has(item.path) || currentFrameworkNames.has(item.path) || previousFiles.has(item.path)) continue;
|
|
119
|
+
const destination = path.join(project, ...item.path.split("/"));
|
|
120
|
+
const absent = item.disposition === "absent";
|
|
121
|
+
const stillMatches = absent ? !fs.existsSync(destination) : matchesFingerprint(destination, item);
|
|
122
|
+
carriedOverrides.push({
|
|
88
123
|
...item,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
})
|
|
92
|
-
.filter((item) => fs.existsSync(item.destination));
|
|
93
|
-
|
|
94
|
-
const differentFiles = frameworkFiles.filter((item) => {
|
|
95
|
-
if (!fs.existsSync(item.destination)) return false;
|
|
96
|
-
return !sameFile(item.source, item.destination);
|
|
97
|
-
});
|
|
98
|
-
const automaticReplacements = differentFiles.filter((item) => {
|
|
99
|
-
const previous = previousFiles.get(posix(item.relative));
|
|
100
|
-
return previous && hashFile(item.destination) === previous.sha256;
|
|
101
|
-
});
|
|
102
|
-
const conflicts = differentFiles.filter((item) => !automaticReplacements.includes(item));
|
|
103
|
-
const automaticStale = staleFrameworkFiles.filter((item) => item.unmodified);
|
|
104
|
-
const staleConflicts = staleFrameworkFiles.filter((item) => !item.unmodified);
|
|
105
|
-
|
|
106
|
-
if ((conflicts.length || staleConflicts.length) && !force) {
|
|
107
|
-
console.error(`NCCGS found ${conflicts.length} modified and ${staleConflicts.length} modified-stale framework file(s):`);
|
|
108
|
-
for (const item of conflicts.slice(0, 30)) console.error(`- ${posix(item.relative)}`);
|
|
109
|
-
for (const item of staleConflicts.slice(0, 30)) console.error(`- stale: ${item.relative}`);
|
|
110
|
-
if (conflicts.length > 30) console.error(`- ... ${conflicts.length - 30} more`);
|
|
111
|
-
console.error("These files differ from the hashes recorded at the previous install.");
|
|
112
|
-
console.error("Review them, then re-run with --force to back up and replace intentional overrides.");
|
|
113
|
-
process.exit(2);
|
|
124
|
+
status: item.status === "approved" && stillMatches ? "approved" : "pending_review",
|
|
125
|
+
reason: stillMatches ? item.reason : "Previously approved override changed and requires migration review."
|
|
126
|
+
});
|
|
114
127
|
}
|
|
115
128
|
|
|
116
|
-
const
|
|
117
|
-
|
|
129
|
+
const overrideRecords = [
|
|
130
|
+
...filePlans.filter((item) => item.override).map((item) => item.override),
|
|
131
|
+
...stalePlans.filter((item) => item.override).map((item) => item.override),
|
|
132
|
+
...carriedOverrides
|
|
133
|
+
].sort((a, b) => a.path.localeCompare(b.path));
|
|
134
|
+
const pendingOverrides = overrideRecords.filter((item) => item.status !== "approved");
|
|
135
|
+
|
|
136
|
+
const newFiles = filePlans.filter((item) => item.action === "install");
|
|
137
|
+
const currentFiles = filePlans.filter((item) => item.action === "current");
|
|
138
|
+
const automaticReplacements = filePlans.filter((item) => item.action === "replace");
|
|
139
|
+
const forcedReplacements = filePlans.filter((item) => item.action === "force-replace");
|
|
140
|
+
const preservedFiles = filePlans.filter((item) => item.action.startsWith("preserve"));
|
|
141
|
+
const automaticStale = stalePlans.filter((item) => item.action === "remove");
|
|
142
|
+
const forcedStale = stalePlans.filter((item) => item.action === "force-remove");
|
|
143
|
+
const preservedStale = stalePlans.filter((item) => item.action === "preserve-stale");
|
|
144
|
+
|
|
118
145
|
const scaffoldFiles = walk(path.join(root, "scaffold")).map((file) => ({
|
|
119
146
|
source: file,
|
|
120
147
|
relative: path.relative(path.join(root, "scaffold"), file),
|
|
121
148
|
destination: path.join(project, path.relative(path.join(root, "scaffold"), file)),
|
|
122
149
|
}));
|
|
150
|
+
const npmExcludedScaffoldFile = ".nccgs/.gitignore";
|
|
151
|
+
if (!scaffoldFiles.some((item) => posix(item.relative) === npmExcludedScaffoldFile)) {
|
|
152
|
+
const fallback = path.join(root, "package-assets", "nccgs-project.gitignore");
|
|
153
|
+
if (fs.existsSync(fallback)) {
|
|
154
|
+
scaffoldFiles.push({
|
|
155
|
+
source: fallback,
|
|
156
|
+
relative: npmExcludedScaffoldFile.split("/").join(path.sep),
|
|
157
|
+
destination: path.join(project, ...npmExcludedScaffoldFile.split("/"))
|
|
158
|
+
});
|
|
159
|
+
scaffoldFiles.sort((a, b) => a.relative.localeCompare(b.relative));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
123
162
|
const newScaffold = scaffoldFiles.filter((item) => !fs.existsSync(item.destination));
|
|
124
163
|
const settingsPlan = planManagedSettings(project);
|
|
125
164
|
const targetPolicyPath = path.join(project, ".nccgs", "project.yaml");
|
|
@@ -132,12 +171,16 @@ if (!new Set(["quality", "balanced", "inherit"]).has(modelProfile)) {
|
|
|
132
171
|
}
|
|
133
172
|
|
|
134
173
|
console.log(`NCCGS installation target: ${project}`);
|
|
135
|
-
console.log(`Framework version: ${previousInstallState?.frameworkVersion ?? "not installed"} -> ${
|
|
136
|
-
console.log(`Framework files: ${frameworkFiles.length} (${newFiles.length} new, ${
|
|
137
|
-
console.log(`Stale framework files: ${automaticStale.length} automatic removal, ${
|
|
174
|
+
console.log(`Framework version: ${previousInstallState?.frameworkVersion ?? "not installed"} -> ${currentVersion}`);
|
|
175
|
+
console.log(`Framework files: ${frameworkFiles.length} (${newFiles.length} new, ${currentFiles.length} current, ${automaticReplacements.length} automatic update, ${preservedFiles.length} preserved override, ${forcedReplacements.length} forced override)`);
|
|
176
|
+
console.log(`Stale framework files: ${automaticStale.length} automatic removal, ${preservedStale.length} preserved override, ${forcedStale.length} forced removal`);
|
|
138
177
|
console.log(`Project scaffold files to initialize: ${newScaffold.length}`);
|
|
139
178
|
console.log(`Managed Claude settings: ${settingsPlan.changed ? "update" : "already current"}`);
|
|
140
179
|
console.log(`Agent model profile: ${modelProfile}`);
|
|
180
|
+
if (pendingOverrides.length) {
|
|
181
|
+
console.log(`${pendingOverrides.length} project override(s) will be preserved for /migrate-project review:`);
|
|
182
|
+
for (const item of pendingOverrides.slice(0, 30)) console.log(`- ${item.path} (${item.disposition})`);
|
|
183
|
+
}
|
|
141
184
|
if (dryRun) {
|
|
142
185
|
console.log("Dry run complete; no files changed.");
|
|
143
186
|
process.exit(0);
|
|
@@ -145,23 +188,21 @@ if (dryRun) {
|
|
|
145
188
|
|
|
146
189
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
147
190
|
const backupRoot = path.join(project, ".nccgs", "backups", stamp);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
for (const item of staleFrameworkFiles) {
|
|
154
|
-
const backup = path.join(backupRoot, ...item.relative.split("/"));
|
|
155
|
-
fs.mkdirSync(path.dirname(backup), { recursive: true });
|
|
156
|
-
fs.copyFileSync(item.destination, backup);
|
|
191
|
+
const replacedFiles = [...automaticReplacements, ...forcedReplacements];
|
|
192
|
+
const removedFiles = [...automaticStale, ...forcedStale];
|
|
193
|
+
for (const item of replacedFiles) backupFile(item.destination, path.join(backupRoot, ...item.relative.split("/")));
|
|
194
|
+
for (const item of removedFiles) {
|
|
195
|
+
backupFile(item.destination, path.join(backupRoot, ...item.relative.split("/")));
|
|
157
196
|
fs.unlinkSync(item.destination);
|
|
158
197
|
}
|
|
198
|
+
if (previousOverrideManifest && JSON.stringify(previousOverrideManifest.overrides ?? []) !== JSON.stringify(overrideRecords)) {
|
|
199
|
+
backupFile(overrideManifestPath, path.join(backupRoot, ".nccgs", "framework-overrides.json"));
|
|
200
|
+
}
|
|
159
201
|
|
|
160
|
-
for (const item of
|
|
202
|
+
for (const item of filePlans.filter((entry) => new Set(["install", "replace", "force-replace"]).has(entry.action))) {
|
|
161
203
|
fs.mkdirSync(path.dirname(item.destination), { recursive: true });
|
|
162
204
|
fs.copyFileSync(item.source, item.destination);
|
|
163
205
|
}
|
|
164
|
-
|
|
165
206
|
for (const item of newScaffold) {
|
|
166
207
|
fs.mkdirSync(path.dirname(item.destination), { recursive: true });
|
|
167
208
|
fs.copyFileSync(item.source, item.destination);
|
|
@@ -170,10 +211,14 @@ for (const item of newScaffold) {
|
|
|
170
211
|
installManagedSettings(settingsPlan, backupRoot);
|
|
171
212
|
installClaudeImport(project, backupRoot);
|
|
172
213
|
|
|
214
|
+
const skipAgentPaths = preservedFiles
|
|
215
|
+
.filter((item) => item.relative.startsWith(".claude/agents/"))
|
|
216
|
+
.flatMap((item) => ["--skip-path", item.relative]);
|
|
173
217
|
const modelConfiguration = spawnSync(process.execPath, [
|
|
174
|
-
path.join(
|
|
218
|
+
path.join(root, ".claude", "nccgs", "tools", "configure-models.mjs"),
|
|
175
219
|
"--project", project,
|
|
176
220
|
"--profile", modelProfile,
|
|
221
|
+
...skipAgentPaths
|
|
177
222
|
], { cwd: project, encoding: "utf8" });
|
|
178
223
|
if (modelConfiguration.status !== 0) {
|
|
179
224
|
process.stderr.write(modelConfiguration.stdout ?? "");
|
|
@@ -182,12 +227,13 @@ if (modelConfiguration.status !== 0) {
|
|
|
182
227
|
process.exit(1);
|
|
183
228
|
}
|
|
184
229
|
|
|
185
|
-
|
|
230
|
+
writeOverrideManifest(overrideManifestPath, overrideRecords, previousOverrideManifest, currentVersion);
|
|
231
|
+
writeInstallState(project, modelProfile, frameworkFiles, installStatePath, previousInstallState, new Set(overrideRecords.map((item) => item.path)));
|
|
186
232
|
|
|
187
233
|
const doctor = spawnSync(process.execPath, [
|
|
188
234
|
path.join(scriptDir, "doctor.mjs"),
|
|
189
235
|
"--project", project,
|
|
190
|
-
"--allow-
|
|
236
|
+
"--allow-migration-required"
|
|
191
237
|
], { cwd: project, encoding: "utf8" });
|
|
192
238
|
process.stdout.write(doctor.stdout ?? "");
|
|
193
239
|
process.stderr.write(doctor.stderr ?? "");
|
|
@@ -197,10 +243,12 @@ if (doctor.status !== 0) {
|
|
|
197
243
|
}
|
|
198
244
|
|
|
199
245
|
console.log("NCCGS installation complete.");
|
|
200
|
-
if (
|
|
246
|
+
if (fs.existsSync(backupRoot)) {
|
|
247
|
+
console.log(`Replaced shared files were backed up under: ${backupRoot}`);
|
|
248
|
+
}
|
|
201
249
|
const installedPolicy = fs.readFileSync(targetPolicyPath, "utf8");
|
|
202
250
|
const installedPolicySchema = Number(installedPolicy.match(/^schema_version:\s*(\d+)\s*$/m)?.[1] ?? 0);
|
|
203
|
-
if (installedPolicySchema !== 2) {
|
|
251
|
+
if (installedPolicySchema !== 2 || pendingOverrides.length) {
|
|
204
252
|
console.log("Project migration required: open a new Claude Code session and run `/migrate-project` once.");
|
|
205
253
|
} else if (!previousInstallState) {
|
|
206
254
|
console.log("First NCCGS adoption: run `/migrate-project` once for an existing project; a new project can start with `/status`.");
|
|
@@ -208,7 +256,75 @@ if (installedPolicySchema !== 2) {
|
|
|
208
256
|
console.log("No project migration required for this NCCGS update.");
|
|
209
257
|
}
|
|
210
258
|
|
|
259
|
+
function planFrameworkFile(item) {
|
|
260
|
+
const previous = previousFiles.get(item.relative);
|
|
261
|
+
const existingOverride = previousOverrides.get(item.relative);
|
|
262
|
+
const upstream = fingerprintFile(item.source);
|
|
263
|
+
if (!fs.existsSync(item.destination)) {
|
|
264
|
+
if (!force && (previous || existingOverride?.disposition === "absent")) {
|
|
265
|
+
return { ...item, action: "preserve-absent", override: buildOverride(item, existingOverride, upstream, "absent") };
|
|
266
|
+
}
|
|
267
|
+
return { ...item, action: "install", override: null };
|
|
268
|
+
}
|
|
269
|
+
if (equivalentFiles(item.source, item.destination)) return { ...item, action: "current", override: null };
|
|
270
|
+
if (!force && previous && matchesFingerprint(item.destination, previous)) return { ...item, action: "replace", override: null };
|
|
271
|
+
if (force) return { ...item, action: "force-replace", override: null };
|
|
272
|
+
return { ...item, action: "preserve", override: buildOverride(item, existingOverride, upstream, "preserve") };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function planStaleFile(previous) {
|
|
276
|
+
const destination = path.join(project, ...previous.relative.split("/"));
|
|
277
|
+
if (!fs.existsSync(destination)) return null;
|
|
278
|
+
if (force) return { ...previous, relative: previous.relative, destination, action: "force-remove", override: null };
|
|
279
|
+
if (matchesFingerprint(destination, previous)) return { ...previous, relative: previous.relative, destination, action: "remove", override: null };
|
|
280
|
+
const existingOverride = previousOverrides.get(previous.relative);
|
|
281
|
+
const current = fingerprintFile(destination);
|
|
282
|
+
const stillApproved = existingOverride?.status === "approved" && matchesFingerprint(destination, existingOverride)
|
|
283
|
+
&& existingOverride.upstreamSha256 === null;
|
|
284
|
+
return {
|
|
285
|
+
...previous,
|
|
286
|
+
relative: previous.relative,
|
|
287
|
+
destination,
|
|
288
|
+
action: "preserve-stale",
|
|
289
|
+
override: {
|
|
290
|
+
path: previous.relative,
|
|
291
|
+
disposition: "preserve",
|
|
292
|
+
status: stillApproved ? "approved" : "pending_review",
|
|
293
|
+
...current,
|
|
294
|
+
upstreamSha256: null,
|
|
295
|
+
upstreamCanonicalSha256: null,
|
|
296
|
+
retiredFromFramework: true,
|
|
297
|
+
reason: "Locally modified file was preserved after its framework path retired; review ownership during migration."
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function buildOverride(item, existing, upstream, disposition) {
|
|
303
|
+
const current = disposition === "absent" ? { sha256: null, canonicalSha256: null } : fingerprintFile(item.destination);
|
|
304
|
+
const currentMatches = disposition === "absent"
|
|
305
|
+
? existing?.disposition === "absent" && !fs.existsSync(item.destination)
|
|
306
|
+
: existing?.disposition === "preserve" && sameFingerprint(current, existing);
|
|
307
|
+
const previousUpstream = existing ? {
|
|
308
|
+
sha256: existing.upstreamSha256,
|
|
309
|
+
canonicalSha256: existing.upstreamCanonicalSha256
|
|
310
|
+
} : null;
|
|
311
|
+
const upstreamMatches = previousUpstream && sameFingerprint(upstream, previousUpstream);
|
|
312
|
+
const approved = existing?.status === "approved" && currentMatches && upstreamMatches;
|
|
313
|
+
return {
|
|
314
|
+
path: item.relative,
|
|
315
|
+
disposition,
|
|
316
|
+
status: approved ? "approved" : "pending_review",
|
|
317
|
+
...current,
|
|
318
|
+
upstreamSha256: upstream.sha256,
|
|
319
|
+
upstreamCanonicalSha256: upstream.canonicalSha256 ?? null,
|
|
320
|
+
reason: approved
|
|
321
|
+
? existing.reason
|
|
322
|
+
: "Project-specific framework change was preserved automatically; review and rebase it with /migrate-project."
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
211
326
|
function walk(directory) {
|
|
327
|
+
if (!fs.existsSync(directory)) return [];
|
|
212
328
|
const output = [];
|
|
213
329
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
214
330
|
const full = path.join(directory, entry.name);
|
|
@@ -218,27 +334,36 @@ function walk(directory) {
|
|
|
218
334
|
return output.sort((a, b) => a.localeCompare(b));
|
|
219
335
|
}
|
|
220
336
|
|
|
221
|
-
function
|
|
222
|
-
|
|
223
|
-
const b = fs.readFileSync(right);
|
|
224
|
-
return a.length === b.length && a.equals(b);
|
|
337
|
+
function posix(value) {
|
|
338
|
+
return value.split(path.sep).join("/");
|
|
225
339
|
}
|
|
226
340
|
|
|
227
|
-
function
|
|
228
|
-
|
|
341
|
+
function isManagedRelative(value) {
|
|
342
|
+
if (typeof value !== "string" || !value.startsWith(".claude/")) return false;
|
|
343
|
+
const parts = value.split("/");
|
|
344
|
+
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
229
345
|
}
|
|
230
346
|
|
|
231
|
-
function
|
|
232
|
-
|
|
347
|
+
function readJsonIfPresent(file, label) {
|
|
348
|
+
if (!fs.existsSync(file)) return null;
|
|
349
|
+
try { return JSON.parse(fs.readFileSync(file, "utf8")); }
|
|
350
|
+
catch (error) {
|
|
351
|
+
console.error(`Invalid ${label} at ${file}: ${error.message}`);
|
|
352
|
+
process.exit(2);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function backupFile(source, destination) {
|
|
357
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
358
|
+
fs.copyFileSync(source, destination);
|
|
233
359
|
}
|
|
234
360
|
|
|
235
361
|
function planManagedSettings(projectRoot) {
|
|
236
362
|
const settingsPath = path.join(projectRoot, ".claude", "settings.json");
|
|
237
363
|
let current = {};
|
|
238
364
|
if (fs.existsSync(settingsPath)) {
|
|
239
|
-
try {
|
|
240
|
-
|
|
241
|
-
} catch (error) {
|
|
365
|
+
try { current = JSON.parse(fs.readFileSync(settingsPath, "utf8")); }
|
|
366
|
+
catch (error) {
|
|
242
367
|
console.error(`Cannot merge invalid JSON settings at ${settingsPath}: ${error.message}`);
|
|
243
368
|
process.exit(2);
|
|
244
369
|
}
|
|
@@ -264,27 +389,37 @@ function planManagedSettings(projectRoot) {
|
|
|
264
389
|
|
|
265
390
|
function installManagedSettings(plan, backupDirectory) {
|
|
266
391
|
if (!plan.changed) return;
|
|
267
|
-
if (plan.existed)
|
|
268
|
-
const backup = path.join(backupDirectory, ".claude", "settings.json");
|
|
269
|
-
fs.mkdirSync(path.dirname(backup), { recursive: true });
|
|
270
|
-
fs.copyFileSync(plan.settingsPath, backup);
|
|
271
|
-
}
|
|
392
|
+
if (plan.existed) backupFile(plan.settingsPath, path.join(backupDirectory, ".claude", "settings.json"));
|
|
272
393
|
fs.mkdirSync(path.dirname(plan.settingsPath), { recursive: true });
|
|
273
394
|
fs.writeFileSync(plan.settingsPath, plan.nextText, "utf8");
|
|
274
395
|
}
|
|
275
396
|
|
|
276
|
-
function
|
|
397
|
+
function writeOverrideManifest(file, overrides, previous, frameworkVersion) {
|
|
398
|
+
if (!overrides.length && !previous) return;
|
|
399
|
+
const now = new Date().toISOString();
|
|
400
|
+
const manifest = {
|
|
401
|
+
schemaVersion: 1,
|
|
402
|
+
frameworkVersion,
|
|
403
|
+
createdAt: previous?.createdAt ?? now,
|
|
404
|
+
updatedAt: now,
|
|
405
|
+
overrides
|
|
406
|
+
};
|
|
407
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
408
|
+
fs.writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function writeInstallState(projectRoot, profile, files, statePath, previousState, overridePaths) {
|
|
277
412
|
const now = new Date().toISOString();
|
|
278
413
|
const state = {
|
|
279
|
-
schemaVersion:
|
|
280
|
-
frameworkVersion:
|
|
414
|
+
schemaVersion: 3,
|
|
415
|
+
frameworkVersion: currentVersion,
|
|
281
416
|
previousFrameworkVersion: previousState?.frameworkVersion ?? null,
|
|
282
417
|
modelProfile: profile,
|
|
283
418
|
installedAt: previousState?.installedAt ?? now,
|
|
284
419
|
updatedAt: now,
|
|
285
420
|
files: files.map((item) => ({
|
|
286
|
-
relative:
|
|
287
|
-
|
|
421
|
+
relative: item.relative,
|
|
422
|
+
...(overridePaths.has(item.relative) ? fingerprintFile(item.source) : fingerprintFile(path.join(projectRoot, ...item.relative.split("/"))))
|
|
288
423
|
}))
|
|
289
424
|
};
|
|
290
425
|
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
@@ -299,12 +434,9 @@ function installClaudeImport(projectRoot, backupDirectory) {
|
|
|
299
434
|
fs.copyFileSync(path.join(root, "CLAUDE.md"), claudePath);
|
|
300
435
|
return;
|
|
301
436
|
}
|
|
302
|
-
|
|
303
437
|
const current = fs.readFileSync(claudePath, "utf8");
|
|
304
438
|
if (current.includes(begin) || current.includes("@.claude/nccgs/constitution.md")) return;
|
|
305
|
-
|
|
306
|
-
fs.mkdirSync(backupDirectory, { recursive: true });
|
|
307
|
-
fs.copyFileSync(claudePath, path.join(backupDirectory, "CLAUDE.md"));
|
|
439
|
+
backupFile(claudePath, path.join(backupDirectory, "CLAUDE.md"));
|
|
308
440
|
const separator = current.endsWith("\n") ? "\n" : "\n\n";
|
|
309
441
|
fs.writeFileSync(claudePath, `${current}${separator}${block}\n`, "utf8");
|
|
310
442
|
}
|
package/scripts/validate.mjs
CHANGED
|
@@ -51,10 +51,18 @@ for (const relative of [
|
|
|
51
51
|
"scaffold/.nccgs/project.yaml", "scaffold/.nccgs/state.md",
|
|
52
52
|
"scaffold/.nccgs/requirements.yaml", "scaffold/.nccgs/templates/context-packet.yaml",
|
|
53
53
|
"scaffold/.nccgs/templates/closure-record.md", "package-assets/setup-vivox-voice-chat-evals.gitignore",
|
|
54
|
+
"package-assets/nccgs-project.gitignore",
|
|
54
55
|
"scripts/cli.mjs", "scripts/doctor.mjs", "scripts/install.mjs",
|
|
55
|
-
"scripts/sync-unity-skills.mjs", "tests/framework.test.mjs"
|
|
56
|
+
"scripts/sync-unity-skills.mjs", "tests/framework.test.mjs",
|
|
57
|
+
".claude/nccgs/tools/integrity.mjs", ".claude/nccgs/tools/approve-overrides.mjs"
|
|
56
58
|
]) requireFile(relative);
|
|
57
59
|
|
|
60
|
+
const scaffoldIgnore = path.join(root, "scaffold/.nccgs/.gitignore");
|
|
61
|
+
const scaffoldIgnoreFallback = path.join(root, "package-assets/nccgs-project.gitignore");
|
|
62
|
+
if (fs.existsSync(scaffoldIgnore) && fs.readFileSync(scaffoldIgnore, "utf8") !== fs.readFileSync(scaffoldIgnoreFallback, "utf8")) {
|
|
63
|
+
errors.push("Scaffold .nccgs/.gitignore differs from its npm fallback asset.");
|
|
64
|
+
}
|
|
65
|
+
|
|
58
66
|
const versions = [
|
|
59
67
|
fs.readFileSync(path.join(root, "VERSION"), "utf8").trim(),
|
|
60
68
|
JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version,
|
|
@@ -128,7 +136,14 @@ for (const command of hookCommands) {
|
|
|
128
136
|
const check = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
|
|
129
137
|
if (check.status !== 0) errors.push(`Hook syntax failure ${match[1]}: ${check.stderr.trim()}`);
|
|
130
138
|
}
|
|
131
|
-
for (const file of [
|
|
139
|
+
for (const file of [
|
|
140
|
+
path.join(root, ".claude/nccgs/tools/configure-models.mjs"),
|
|
141
|
+
path.join(root, ".claude/nccgs/tools/integrity.mjs"),
|
|
142
|
+
path.join(root, ".claude/nccgs/tools/approve-overrides.mjs"),
|
|
143
|
+
path.join(root, "scripts/cli.mjs"),
|
|
144
|
+
path.join(root, "scripts/doctor.mjs"),
|
|
145
|
+
path.join(root, "scripts/install.mjs")
|
|
146
|
+
]) {
|
|
132
147
|
const check = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
|
|
133
148
|
if (check.status !== 0) errors.push(`Script syntax failure ${posix(path.relative(root, file))}: ${check.stderr.trim()}`);
|
|
134
149
|
}
|
package/tests/framework.test.mjs
CHANGED
|
@@ -129,7 +129,7 @@ test("installer preserves project truth, merges settings, and applies inherit pr
|
|
|
129
129
|
}
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
-
test("update replaces untouched
|
|
132
|
+
test("update replaces untouched files, preserves local overrides, and doctor gates review", () => {
|
|
133
133
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-idempotent-update-test-"));
|
|
134
134
|
try {
|
|
135
135
|
for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(temp, marker));
|
|
@@ -148,20 +148,84 @@ test("update replaces untouched managed files, blocks local edits, and doctor ve
|
|
|
148
148
|
|
|
149
149
|
const update = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "update"], { cwd: temp, encoding: "utf8" });
|
|
150
150
|
assert.equal(update.status, 0, update.stderr || update.stdout);
|
|
151
|
-
assert.match(update.stdout,
|
|
151
|
+
assert.match(update.stdout, new RegExp(`1\\.0\\.0 -> ${frameworkVersion.replace(/\./g, "\\.")}`));
|
|
152
152
|
assert.match(update.stdout, /automatic update/);
|
|
153
153
|
assert.match(update.stdout, /No project migration required/);
|
|
154
154
|
assert.equal(fs.readFileSync(managedPath, "utf8"), fs.readFileSync(path.join(root, managedRelative), "utf8"));
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
let doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
|
|
157
157
|
assert.equal(doctor.status, 0, doctor.stderr || doctor.stdout);
|
|
158
158
|
assert.match(doctor.stdout, /NCCGS doctor passed/);
|
|
159
159
|
|
|
160
160
|
fs.writeFileSync(managedPath, "project-local override\n", "utf8");
|
|
161
|
-
const
|
|
162
|
-
assert.equal(
|
|
163
|
-
assert.match(
|
|
161
|
+
const preserved = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "update"], { cwd: temp, encoding: "utf8" });
|
|
162
|
+
assert.equal(preserved.status, 0, preserved.stderr || preserved.stdout);
|
|
163
|
+
assert.match(preserved.stdout, /preserved override/);
|
|
164
|
+
assert.match(preserved.stdout, /Project migration required/);
|
|
164
165
|
assert.equal(fs.readFileSync(managedPath, "utf8"), "project-local override\n");
|
|
166
|
+
|
|
167
|
+
const overridePath = path.join(temp, ".nccgs/framework-overrides.json");
|
|
168
|
+
const overrides = JSON.parse(fs.readFileSync(overridePath, "utf8"));
|
|
169
|
+
const override = overrides.overrides.find((item) => item.path === managedRelative);
|
|
170
|
+
assert.equal(override.status, "pending_review");
|
|
171
|
+
assert.equal(override.disposition, "preserve");
|
|
172
|
+
|
|
173
|
+
doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
|
|
174
|
+
assert.equal(doctor.status, 1);
|
|
175
|
+
assert.match(doctor.stderr, /awaits \/migrate-project review/);
|
|
176
|
+
|
|
177
|
+
const approve = spawnSync(process.execPath, [path.join(temp, ".claude/nccgs/tools/approve-overrides.mjs"), "--project", temp, "--approve-pending"], { cwd: temp, encoding: "utf8" });
|
|
178
|
+
assert.equal(approve.status, 0, approve.stderr || approve.stdout);
|
|
179
|
+
doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
|
|
180
|
+
assert.equal(doctor.status, 0, doctor.stderr || doctor.stdout);
|
|
181
|
+
} finally {
|
|
182
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("doctor accepts a legacy CRLF raw hash after Git normalizes the file to LF", () => {
|
|
187
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-eol-test-"));
|
|
188
|
+
try {
|
|
189
|
+
for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(temp, marker));
|
|
190
|
+
const install = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "install"], { cwd: temp, encoding: "utf8" });
|
|
191
|
+
assert.equal(install.status, 0, install.stderr || install.stdout);
|
|
192
|
+
|
|
193
|
+
const relative = ".claude/nccgs/constitution.md";
|
|
194
|
+
const file = path.join(temp, ...relative.split("/"));
|
|
195
|
+
const lf = fs.readFileSync(file, "utf8").replace(/\r\n?/g, "\n");
|
|
196
|
+
fs.writeFileSync(file, lf, "utf8");
|
|
197
|
+
const statePath = path.join(temp, ".claude/nccgs/install-state.json");
|
|
198
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
199
|
+
const record = state.files.find((item) => item.relative === relative);
|
|
200
|
+
record.sha256 = sha256(lf.replace(/\n/g, "\r\n"));
|
|
201
|
+
delete record.canonicalSha256;
|
|
202
|
+
state.schemaVersion = 2;
|
|
203
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
204
|
+
|
|
205
|
+
const doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
|
|
206
|
+
assert.equal(doctor.status, 0, doctor.stderr || doctor.stdout);
|
|
207
|
+
} finally {
|
|
208
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("update preserves an intentionally missing managed file until migration review", () => {
|
|
213
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-absent-override-test-"));
|
|
214
|
+
try {
|
|
215
|
+
for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(temp, marker));
|
|
216
|
+
const install = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "install"], { cwd: temp, encoding: "utf8" });
|
|
217
|
+
assert.equal(install.status, 0, install.stderr || install.stdout);
|
|
218
|
+
|
|
219
|
+
const relative = ".claude/skills/status/SKILL.md";
|
|
220
|
+
const file = path.join(temp, ...relative.split("/"));
|
|
221
|
+
fs.unlinkSync(file);
|
|
222
|
+
const update = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "update"], { cwd: temp, encoding: "utf8" });
|
|
223
|
+
assert.equal(update.status, 0, update.stderr || update.stdout);
|
|
224
|
+
assert.ok(!fs.existsSync(file));
|
|
225
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(temp, ".nccgs/framework-overrides.json"), "utf8"));
|
|
226
|
+
const override = manifest.overrides.find((item) => item.path === relative);
|
|
227
|
+
assert.equal(override.disposition, "absent");
|
|
228
|
+
assert.equal(override.status, "pending_review");
|
|
165
229
|
} finally {
|
|
166
230
|
fs.rmSync(temp, { recursive: true, force: true });
|
|
167
231
|
}
|