@nimiplatform/nimi-coding 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/CONTRIBUTING.md +3 -3
- package/README.md +285 -41
- package/README.zh-CN.md +294 -31
- package/bin/nimicoding.mjs +1 -1
- package/cli/commands/authority.mjs +849 -0
- package/cli/commands/clear.mjs +27 -7
- package/cli/commands/doctor.mjs +10 -1
- package/cli/commands/start.mjs +23 -27
- package/cli/commands/sync.mjs +21 -101
- package/cli/constants.mjs +7 -31
- package/cli/dev/check-authority-surface.mjs +173 -0
- package/cli/dev/check-product-conformance.mjs +917 -0
- package/cli/help.mjs +10 -17
- package/cli/index.mjs +6 -47
- package/cli/lib/authority/anchors.mjs +491 -0
- package/cli/lib/authority/audit.mjs +490 -0
- package/cli/lib/authority/closed-sets.mjs +646 -0
- package/cli/lib/authority/compile.mjs +110 -0
- package/cli/lib/authority/consumers.mjs +470 -0
- package/cli/lib/authority/diagnostics.mjs +87 -0
- package/cli/lib/authority/diff.mjs +287 -0
- package/cli/lib/authority/discover.mjs +447 -0
- package/cli/lib/authority/evidence-snapshot.mjs +1 -0
- package/cli/lib/authority/evidence.mjs +1432 -0
- package/cli/lib/authority/format.mjs +197 -0
- package/cli/lib/authority/git-snapshot.mjs +1065 -0
- package/cli/lib/authority/graph.mjs +351 -0
- package/cli/lib/authority/impact.mjs +252 -0
- package/cli/lib/authority/lexicon.mjs +55 -0
- package/cli/lib/authority/query.mjs +162 -0
- package/cli/lib/authority/repository-corpus.mjs +37 -0
- package/cli/lib/authority/repository-inventory.mjs +34 -0
- package/cli/lib/authority/review.mjs +222 -0
- package/cli/lib/authority/sarif.mjs +181 -0
- package/cli/lib/authority/scope-bindings.mjs +225 -0
- package/cli/lib/authority/source-map.mjs +105 -0
- package/cli/lib/authority/source-markdown.mjs +167 -0
- package/cli/lib/authority/source-yaml.mjs +312 -0
- package/cli/lib/authority/surface.mjs +79 -0
- package/cli/lib/authority/terms.mjs +421 -0
- package/cli/lib/authority/tracked-text.mjs +70 -0
- package/cli/lib/authority/validate.mjs +308 -0
- package/cli/lib/bootstrap.mjs +55 -71
- package/cli/lib/doctor.mjs +40 -123
- package/cli/lib/entrypoints.mjs +103 -105
- package/cli/lib/fs-helpers.mjs +139 -15
- package/cli/lib/internal/governance/config.mjs +0 -36
- package/cli/lib/shared.mjs +8 -21
- package/cli/lib/sync.mjs +81 -92
- package/cli/lib/value-helpers.mjs +0 -26
- package/cli/lib/yaml-helpers.mjs +1 -124
- package/cli/seeds/bootstrap.mjs +31 -87
- package/cli/seeds/seed-policy.yaml +9 -63
- package/contracts/authority-evidence-bindings.schema.yaml +53 -0
- package/contracts/authority-evidence-probe-results.schema.yaml +38 -0
- package/contracts/authority-impact-dispositions.schema.yaml +13 -0
- package/contracts/authority-scope-bindings.schema.yaml +15 -0
- package/contracts/authority-source.schema.yaml +64 -0
- package/contracts/authority-verifier-bindings.schema.yaml +20 -0
- package/methodology/authority-authoring.yaml +144 -0
- package/methodology/core.yaml +31 -26
- package/package.json +10 -6
- package/spec/authority-anchors.yaml +99 -0
- package/spec/authority-audit.yaml +85 -0
- package/spec/authority-authoring.yaml +150 -0
- package/spec/authority-closed-sets.yaml +65 -0
- package/spec/authority-consumers.yaml +70 -0
- package/spec/authority-context.yaml +50 -0
- package/spec/authority-discovery.yaml +119 -0
- package/spec/authority-evidence.yaml +200 -0
- package/spec/authority-graph.yaml +59 -0
- package/spec/authority-impact.yaml +84 -0
- package/spec/authority-review.yaml +89 -0
- package/spec/authority-terms.yaml +59 -0
- package/spec/product-scope.yaml +36 -23
- package/cli/commands/blueprint-audit.mjs +0 -91
- package/cli/commands/classify-spec-tree.mjs +0 -5
- package/cli/commands/generate-spec-derived-docs.mjs +0 -151
- package/cli/commands/generate-spec-migration-plan.mjs +0 -30
- package/cli/commands/surface-validator-command.mjs +0 -49
- package/cli/commands/validate-domain-admission.mjs +0 -5
- package/cli/commands/validate-guidance-bodies.mjs +0 -5
- package/cli/commands/validate-placement.mjs +0 -5
- package/cli/commands/validate-projection-edges.mjs +0 -5
- package/cli/commands/validate-spec-audit.mjs +0 -27
- package/cli/commands/validate-spec-governance.mjs +0 -144
- package/cli/commands/validate-spec-tree.mjs +0 -27
- package/cli/commands/validate-table-family.mjs +0 -5
- package/cli/commands/validate-tracked-output-admission.mjs +0 -5
- package/cli/lib/blueprint-audit.mjs +0 -363
- package/cli/lib/contracts.mjs +0 -21
- package/cli/lib/internal/contracts-loaders.mjs +0 -47
- package/cli/lib/internal/contracts-parse.mjs +0 -192
- package/cli/lib/internal/governance/runner.mjs +0 -35
- package/cli/lib/internal/surface-taxonomy-validators.mjs +0 -1147
- package/cli/lib/internal/validators-shared.mjs +0 -12
- package/cli/lib/internal/validators-spec-helpers.mjs +0 -52
- package/cli/lib/internal/validators-spec.mjs +0 -380
- package/cli/lib/validators.mjs +0 -30
- package/config/bootstrap.yaml +0 -6
- package/config/spec-generation-inputs.yaml +0 -41
- package/contracts/domain-admission.schema.yaml +0 -56
- package/contracts/migration-inventory.schema.yaml +0 -15
- package/contracts/negative-fixtures.yaml +0 -26
- package/contracts/placement-contract.schema.yaml +0 -127
- package/contracts/projection-edge.schema.yaml +0 -46
- package/contracts/shared-enums.yaml +0 -63
- package/contracts/spec-generation-audit.schema.yaml +0 -46
- package/contracts/spec-generation-inputs.schema.yaml +0 -43
- package/contracts/spec-layout.schema.yaml +0 -32
- package/contracts/surface-taxonomy.schema.yaml +0 -139
- package/contracts/table-family.schema.yaml +0 -133
- package/contracts/tracked-output-admission.schema.yaml +0 -17
- package/methodology/four-closure-policy.yaml +0 -20
- package/methodology/role-separation-policy.yaml +0 -22
- package/methodology/spec-reconstruction.yaml +0 -69
- package/spec/_meta/spec-tree-model.yaml +0 -44
package/cli/lib/doctor.mjs
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import { SPEC_GENERATION_AUDIT_REF } from "../constants.mjs";
|
|
5
4
|
import { getBootstrapSeedEntries } from "../seeds/bootstrap.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import { loadSpecGenerationInputsConfig } from "./contracts.mjs";
|
|
8
|
-
import { pathExists, readTextIfFile } from "./fs-helpers.mjs";
|
|
5
|
+
import { pathExists, preflightManagedProjectPaths } from "./fs-helpers.mjs";
|
|
9
6
|
import { runSeedSync, SYNC_MODE } from "./sync.mjs";
|
|
10
7
|
import { localize, styleHeading, styleLabel, styleStatus } from "./ui.mjs";
|
|
11
8
|
|
|
@@ -15,111 +12,47 @@ function check(id, ok, severity, detail) {
|
|
|
15
12
|
|
|
16
13
|
async function directoryHasFiles(root) {
|
|
17
14
|
const info = await pathExists(root);
|
|
18
|
-
if (!info
|
|
19
|
-
|
|
20
|
-
return entries.length > 0;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async function managedEntrypoints(projectRoot) {
|
|
24
|
-
const refs = [];
|
|
25
|
-
for (const ref of ["AGENTS.md", "CLAUDE.md"]) {
|
|
26
|
-
const text = await readTextIfFile(path.join(projectRoot, ref));
|
|
27
|
-
if (text?.includes("nimicoding:managed:")) refs.push(ref);
|
|
28
|
-
}
|
|
29
|
-
return refs;
|
|
15
|
+
if (!info?.isDirectory()) return false;
|
|
16
|
+
return (await readdir(root)).length > 0;
|
|
30
17
|
}
|
|
31
18
|
|
|
32
19
|
export async function inspectDoctorState(projectRoot) {
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
? "bootstrap contract is supported"
|
|
41
|
-
: "bootstrap contract is missing or unsupported",
|
|
42
|
-
));
|
|
43
|
-
|
|
44
|
-
let seedSync = null;
|
|
45
|
-
if (bootstrap.status === "supported") {
|
|
46
|
-
seedSync = await runSeedSync(projectRoot, SYNC_MODE.CHECK);
|
|
47
|
-
checks.push(check(
|
|
48
|
-
"managed_projection",
|
|
49
|
-
seedSync.ok,
|
|
20
|
+
await preflightManagedProjectPaths(projectRoot, { includeSpec: true });
|
|
21
|
+
const managedProjection = await runSeedSync(projectRoot, SYNC_MODE.CHECK);
|
|
22
|
+
const specPresent = await directoryHasFiles(path.join(projectRoot, ".nimi/spec"));
|
|
23
|
+
const checks = [
|
|
24
|
+
check(
|
|
25
|
+
"managed_surfaces",
|
|
26
|
+
managedProjection.ok,
|
|
50
27
|
"error",
|
|
51
|
-
|
|
52
|
-
?
|
|
53
|
-
: `${
|
|
28
|
+
managedProjection.ok
|
|
29
|
+
? "exact package projection and managed instruction blocks are aligned"
|
|
30
|
+
: `${managedProjection.checkFailures.length} exact managed or deprecated surface issue(s) require attention`,
|
|
31
|
+
),
|
|
32
|
+
check(
|
|
33
|
+
"canonical_authority",
|
|
34
|
+
true,
|
|
35
|
+
"info",
|
|
36
|
+
specPresent
|
|
37
|
+
? "canonical authority is present; run nimicoding authority check .nimi/spec for admission"
|
|
38
|
+
: "canonical authority has not been authored; the project owns all product semantics under .nimi/spec",
|
|
39
|
+
),
|
|
40
|
+
];
|
|
41
|
+
if (process.platform === "win32") {
|
|
42
|
+
checks.push(check(
|
|
43
|
+
"snapshot_no_follow_capability",
|
|
44
|
+
false,
|
|
45
|
+
"info",
|
|
46
|
+
"win32 does not expose O_NOFOLLOW/O_DIRECTORY; snapshot no-follow hardening is downgraded to surrounding lstat/realpath validation",
|
|
54
47
|
));
|
|
55
48
|
}
|
|
56
|
-
|
|
57
|
-
const generationInputs = await loadSpecGenerationInputsConfig(projectRoot);
|
|
58
|
-
checks.push(check(
|
|
59
|
-
"spec_generation_inputs",
|
|
60
|
-
generationInputs.ok && generationInputs.mode === "class_filtered",
|
|
61
|
-
"error",
|
|
62
|
-
generationInputs.ok && generationInputs.mode === "class_filtered"
|
|
63
|
-
? "spec generation inputs use class-filtered authority"
|
|
64
|
-
: "spec generation inputs are missing or invalid",
|
|
65
|
-
));
|
|
66
|
-
|
|
67
|
-
const specRoot = path.join(projectRoot, ".nimi", "spec");
|
|
68
|
-
const specPresent = await directoryHasFiles(specRoot);
|
|
69
|
-
checks.push(check(
|
|
70
|
-
"spec_tree",
|
|
71
|
-
true,
|
|
72
|
-
"info",
|
|
73
|
-
specPresent
|
|
74
|
-
? "canonical spec tree is present; conformance is checked by validate-spec-tree or project-specific validators"
|
|
75
|
-
: "canonical spec tree has not been constructed yet",
|
|
76
|
-
));
|
|
77
|
-
|
|
78
|
-
const auditPath = path.join(projectRoot, SPEC_GENERATION_AUDIT_REF);
|
|
79
|
-
const auditPresent = Boolean(await pathExists(auditPath));
|
|
80
|
-
checks.push(check(
|
|
81
|
-
"spec_generation_audit",
|
|
82
|
-
true,
|
|
83
|
-
"info",
|
|
84
|
-
auditPresent
|
|
85
|
-
? "spec generation audit is present; conformance is checked by validate-spec-audit"
|
|
86
|
-
: "spec generation audit is not present",
|
|
87
|
-
));
|
|
88
|
-
|
|
89
|
-
const entrypoints = await managedEntrypoints(projectRoot);
|
|
90
|
-
checks.push(check(
|
|
91
|
-
"entrypoint_integration",
|
|
92
|
-
true,
|
|
93
|
-
"info",
|
|
94
|
-
entrypoints.length > 0
|
|
95
|
-
? `managed methodology guidance is present in ${entrypoints.join(", ")}`
|
|
96
|
-
: "managed methodology guidance is not installed",
|
|
97
|
-
));
|
|
98
|
-
|
|
99
49
|
const seedEntries = await getBootstrapSeedEntries();
|
|
100
|
-
const ok = checks.every((entry) => entry.severity !== "error");
|
|
101
|
-
|
|
102
50
|
return {
|
|
103
|
-
ok,
|
|
51
|
+
ok: checks.every((entry) => entry.severity !== "error"),
|
|
104
52
|
projectRoot,
|
|
105
|
-
bootstrap,
|
|
106
|
-
managedProjection
|
|
107
|
-
|
|
108
|
-
ok: generationInputs.ok,
|
|
109
|
-
mode: generationInputs.mode ?? null,
|
|
110
|
-
canonicalTargetRoot: generationInputs.canonicalTargetRoot ?? null,
|
|
111
|
-
},
|
|
112
|
-
spec: {
|
|
113
|
-
present: specPresent,
|
|
114
|
-
ok: null,
|
|
115
|
-
summary: null,
|
|
116
|
-
},
|
|
117
|
-
generationAudit: {
|
|
118
|
-
present: auditPresent,
|
|
119
|
-
ok: null,
|
|
120
|
-
summary: null,
|
|
121
|
-
},
|
|
122
|
-
entrypoints,
|
|
53
|
+
bootstrap: { status: "exact_projection_registry" },
|
|
54
|
+
managedProjection,
|
|
55
|
+
spec: { present: specPresent, ok: null, summary: null },
|
|
123
56
|
managedFileCount: seedEntries.length,
|
|
124
57
|
checks,
|
|
125
58
|
};
|
|
@@ -131,33 +64,17 @@ export function formatDoctorResult(result, options = {}) {
|
|
|
131
64
|
"",
|
|
132
65
|
styleLabel(localize("Summary:", "摘要:")),
|
|
133
66
|
` - ${localize("status", "状态")}: ${styleStatus(result.ok ? "ok" : "needs_attention")}`,
|
|
134
|
-
` -
|
|
135
|
-
` -
|
|
136
|
-
` -
|
|
137
|
-
` - generation_audit: ${!result.generationAudit.present ? "not_present" : "present_not_validated"}`,
|
|
67
|
+
` - projection_policy: ${result.bootstrap.status}`,
|
|
68
|
+
` - managed_surfaces: ${result.managedProjection.ok ? "aligned" : "invalid"}`,
|
|
69
|
+
` - canonical_authority: ${result.spec.present ? "present_not_validated" : "not_authored"}`,
|
|
138
70
|
"",
|
|
139
71
|
styleLabel(localize("Checks:", "检查项:")),
|
|
140
72
|
];
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
: result.checks.filter((entry) => entry.severity !== "ok" || !entry.ok);
|
|
145
|
-
if (visibleChecks.length === 0) {
|
|
146
|
-
lines.push(` - ${localize("No blocking issues found.", "没有发现阻塞问题。")}`);
|
|
147
|
-
} else {
|
|
148
|
-
for (const entry of visibleChecks) {
|
|
149
|
-
const marker = entry.ok ? "info" : entry.severity;
|
|
150
|
-
lines.push(` - [${marker}] ${entry.detail}`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
73
|
+
const visible = options.verbose ? result.checks : result.checks.filter((entry) => entry.severity !== "ok" || !entry.ok);
|
|
74
|
+
if (visible.length === 0) lines.push(` - ${localize("No blocking integration issues found.", "没有发现阻塞性的集成问题。")}`);
|
|
75
|
+
else for (const entry of visible) lines.push(` - [${entry.ok ? "info" : entry.severity}] ${entry.detail}`);
|
|
154
76
|
if (!result.spec.present) {
|
|
155
|
-
lines.push(
|
|
156
|
-
"",
|
|
157
|
-
styleLabel(localize("Next:", "下一步:")),
|
|
158
|
-
" - Build canonical product authority under .nimi/spec, then run nimicoding validate-spec-tree.",
|
|
159
|
-
);
|
|
77
|
+
lines.push("", styleLabel(localize("Next:", "下一步:")), " - Author canonical authority under .nimi/spec, format changed files, then run nimicoding authority check .nimi/spec.");
|
|
160
78
|
}
|
|
161
|
-
|
|
162
79
|
return `${lines.join("\n")}\n`;
|
|
163
80
|
}
|
package/cli/lib/entrypoints.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
import {
|
|
@@ -7,101 +7,79 @@ import {
|
|
|
7
7
|
CLAUDE_BEGIN,
|
|
8
8
|
CLAUDE_END,
|
|
9
9
|
} from "../constants.mjs";
|
|
10
|
-
import { pathExists } from "./fs-helpers.mjs";
|
|
10
|
+
import { pathExists, preflightManagedProjectPaths, readUtf8FileFatal } from "./fs-helpers.mjs";
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
export class ManagedBlockError extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "ManagedBlockError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function authorityAuthoringLines() {
|
|
13
20
|
return [
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"",
|
|
17
|
-
"-
|
|
18
|
-
"-
|
|
19
|
-
"-
|
|
20
|
-
"-
|
|
21
|
-
"- Keep
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
"- Product authority lives under `.nimi/spec/**`.",
|
|
22
|
+
"- For canonical authority authoring, read only `.nimi/methodology/authority-authoring.yaml`, the affected authority files or bounded task context, and CLI diagnostics.",
|
|
23
|
+
"- Use `nimicoding authority context <path> <id> --max-units <n> --max-bytes <n> --json` only for the complete declared outgoing interpretation closure; it is not complete task context, and failure never permits guessed or partial context.",
|
|
24
|
+
"- Use `nimicoding authority diff` and `authority impact` with explicit `--max-bytes`; impact reports declared review obligations and does not prove implementation, consumers, or tests are synchronized.",
|
|
25
|
+
"- Under `.nimi/spec/**`, author only closed multi-unit `*.authority.yaml` containers or single-unit `*.authority.md`; historical document formats are unsupported and never inferred.",
|
|
26
|
+
"- Run `nimicoding authority fmt` on each changed file, then `nimicoding authority check` on the complete authority input set.",
|
|
27
|
+
"- Never bypass a failure with inferred or fallback semantics; choose repair values only from product/task authority.",
|
|
28
|
+
"- Keep derived and verification evidence under `.nimi/local/**`; it is never product authority.",
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function managedAgentsBlock() {
|
|
33
|
+
return [AGENTS_BEGIN, "# Nimi Coding Managed Block", "", ...authorityAuthoringLines(), AGENTS_END].join("\n");
|
|
25
34
|
}
|
|
26
35
|
function managedClaudeBlock() {
|
|
27
|
-
return [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"Nimi Coding owns methodology, canonical spec construction, and deterministic validation only.",
|
|
39
|
-
"Keep local generation evidence outside product authority.",
|
|
40
|
-
CLAUDE_END,
|
|
41
|
-
].join("\n");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function escapeRegExp(value) {
|
|
45
|
-
return value.replace(/[.*+?^$()|[\]{}\\]/g, "\\$&");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function blockPattern(begin, end, leading = false) {
|
|
49
|
-
return new RegExp(
|
|
50
|
-
`${leading ? "(?:\\n\\n)?" : ""}${escapeRegExp(begin)}[\\s\\S]*?${escapeRegExp(end)}${leading ? "(?:\\n)?" : ""}`,
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function upsertManagedBlock(existing, begin, end, block) {
|
|
55
|
-
const pattern = blockPattern(begin, end);
|
|
56
|
-
if (pattern.test(existing)) {
|
|
57
|
-
return existing.replace(pattern, block);
|
|
36
|
+
return [CLAUDE_BEGIN, "# Nimi Coding Managed Block", "", ...authorityAuthoringLines(), CLAUDE_END].join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function occurrenceIndexes(text, marker) {
|
|
40
|
+
const indexes = [];
|
|
41
|
+
let offset = 0;
|
|
42
|
+
while (offset <= text.length) {
|
|
43
|
+
const index = text.indexOf(marker, offset);
|
|
44
|
+
if (index === -1) break;
|
|
45
|
+
indexes.push(index);
|
|
46
|
+
offset = index + marker.length;
|
|
58
47
|
}
|
|
59
|
-
|
|
60
|
-
return `${existing}${prefix}${block}\n`;
|
|
48
|
+
return indexes;
|
|
61
49
|
}
|
|
62
50
|
|
|
63
|
-
function
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
51
|
+
export function analyzeManagedBlockText(existing, begin, end, expectedBlock, label = "managed block") {
|
|
52
|
+
const begins = occurrenceIndexes(existing, begin);
|
|
53
|
+
const ends = occurrenceIndexes(existing, end);
|
|
54
|
+
if (begins.length === 0 && ends.length === 0) return { state: "missing", begin: -1, end: -1, exact: false };
|
|
55
|
+
if (begins.length !== 1 || ends.length !== 1) {
|
|
56
|
+
throw new ManagedBlockError(`${label} markers must contain exactly one ordered, non-nested begin/end pair`);
|
|
67
57
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
.replace(/^\n+/, "")
|
|
72
|
-
.trimEnd();
|
|
58
|
+
if (begins[0] >= ends[0]) throw new ManagedBlockError(`${label} markers must contain exactly one ordered, non-nested begin/end pair`);
|
|
59
|
+
const endOffset = ends[0] + end.length;
|
|
60
|
+
const actualBlock = existing.slice(begins[0], endOffset);
|
|
73
61
|
return {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
62
|
+
state: actualBlock === expectedBlock ? "exact" : "drifted",
|
|
63
|
+
begin: begins[0],
|
|
64
|
+
end: endOffset,
|
|
65
|
+
exact: actualBlock === expectedBlock,
|
|
77
66
|
};
|
|
78
67
|
}
|
|
79
68
|
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function upsertTextFile(filePath, block, begin, end, header) {
|
|
87
|
-
const existing = (await pathExists(filePath)) ? await readFile(filePath, "utf8") : header;
|
|
88
|
-
const computed = computeTextFileUpdate(existing, block, begin, end, header);
|
|
89
|
-
if (!computed.changed) return false;
|
|
90
|
-
await writeFile(filePath, computed.next, "utf8");
|
|
91
|
-
return true;
|
|
69
|
+
function upsertManagedBlock(existing, analysis, block) {
|
|
70
|
+
if (analysis.state === "missing") {
|
|
71
|
+
const prefix = existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
72
|
+
return `${existing}${prefix}${block}\n`;
|
|
73
|
+
}
|
|
74
|
+
return `${existing.slice(0, analysis.begin)}${block}${existing.slice(analysis.end)}`;
|
|
92
75
|
}
|
|
93
76
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await rm(filePath, { force: true });
|
|
101
|
-
return { changed: true, removedFile: true };
|
|
102
|
-
}
|
|
103
|
-
await writeFile(filePath, computed.next, "utf8");
|
|
104
|
-
return { changed: true, removedFile: false };
|
|
77
|
+
function removeManagedBlock(existing, analysis) {
|
|
78
|
+
if (analysis.state === "missing") return { changed: false, next: existing };
|
|
79
|
+
return {
|
|
80
|
+
changed: true,
|
|
81
|
+
next: `${existing.slice(0, analysis.begin)}${existing.slice(analysis.end)}`,
|
|
82
|
+
};
|
|
105
83
|
}
|
|
106
84
|
|
|
107
85
|
function entrypointDefinitions(projectRoot) {
|
|
@@ -125,46 +103,66 @@ function entrypointDefinitions(projectRoot) {
|
|
|
125
103
|
];
|
|
126
104
|
}
|
|
127
105
|
|
|
106
|
+
async function inspectDefinition(file) {
|
|
107
|
+
const info = await pathExists(file.path);
|
|
108
|
+
const existing = info?.isFile() ? await readUtf8FileFatal(file.path, file.relativePath) : null;
|
|
109
|
+
const base = existing ?? file.header;
|
|
110
|
+
const analysis = analyzeManagedBlockText(base, file.begin, file.end, file.block, `${file.relativePath} nimicoding managed block`);
|
|
111
|
+
const next = upsertManagedBlock(base, analysis, file.block);
|
|
112
|
+
return { ...file, existing, base, analysis, next, changed: next !== base };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function inspectEntrypointIntegration(projectRoot) {
|
|
116
|
+
await preflightManagedProjectPaths(projectRoot);
|
|
117
|
+
const states = [];
|
|
118
|
+
for (const file of entrypointDefinitions(projectRoot)) states.push(await inspectDefinition(file));
|
|
119
|
+
return states;
|
|
120
|
+
}
|
|
121
|
+
|
|
128
122
|
export async function previewEntrypointIntegration(projectRoot) {
|
|
129
|
-
|
|
130
|
-
for (const file of entrypointDefinitions(projectRoot)) {
|
|
131
|
-
const existing = (await pathExists(file.path)) ? await readFile(file.path, "utf8") : null;
|
|
132
|
-
if (computeTextFileUpdate(existing, file.block, file.begin, file.end, file.header).changed) {
|
|
133
|
-
updates.push(file.relativePath);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return updates;
|
|
123
|
+
return (await inspectEntrypointIntegration(projectRoot)).filter((state) => state.changed).map((state) => state.relativePath);
|
|
137
124
|
}
|
|
138
125
|
|
|
139
126
|
export async function integrateEntrypoints(projectRoot) {
|
|
127
|
+
const states = await inspectEntrypointIntegration(projectRoot);
|
|
140
128
|
const updated = [];
|
|
141
|
-
for (const
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
129
|
+
for (const state of states) {
|
|
130
|
+
if (!state.changed) continue;
|
|
131
|
+
await writeFile(state.path, state.next, "utf8");
|
|
132
|
+
updated.push(state.relativePath);
|
|
145
133
|
}
|
|
146
134
|
return updated;
|
|
147
135
|
}
|
|
148
136
|
|
|
149
|
-
|
|
150
|
-
|
|
137
|
+
async function inspectEntrypointRemoval(projectRoot) {
|
|
138
|
+
await preflightManagedProjectPaths(projectRoot);
|
|
139
|
+
const states = [];
|
|
151
140
|
for (const file of entrypointDefinitions(projectRoot)) {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
141
|
+
const info = await pathExists(file.path);
|
|
142
|
+
const existing = info?.isFile() ? await readUtf8FileFatal(file.path, file.relativePath) : null;
|
|
143
|
+
if (existing === null) {
|
|
144
|
+
states.push({ ...file, existing, removal: { changed: false, next: "" } });
|
|
145
|
+
continue;
|
|
155
146
|
}
|
|
147
|
+
const analysis = analyzeManagedBlockText(existing, file.begin, file.end, file.block, `${file.relativePath} nimicoding managed block`);
|
|
148
|
+
const removal = removeManagedBlock(existing, analysis);
|
|
149
|
+
states.push({ ...file, existing, removal });
|
|
156
150
|
}
|
|
157
|
-
return
|
|
151
|
+
return states;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function previewEntrypointRemoval(projectRoot) {
|
|
155
|
+
return (await inspectEntrypointRemoval(projectRoot)).filter((state) => state.removal.changed).map((state) => state.relativePath);
|
|
158
156
|
}
|
|
159
157
|
|
|
160
158
|
export async function removeManagedEntrypoints(projectRoot) {
|
|
159
|
+
const states = await inspectEntrypointRemoval(projectRoot);
|
|
161
160
|
const updatedFiles = [];
|
|
162
161
|
const removedFiles = [];
|
|
163
|
-
for (const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
162
|
+
for (const state of states) {
|
|
163
|
+
if (!state.removal.changed) continue;
|
|
164
|
+
await writeFile(state.path, state.removal.next, "utf8");
|
|
165
|
+
updatedFiles.push(state.relativePath);
|
|
168
166
|
}
|
|
169
167
|
return { updatedFiles, removedFiles };
|
|
170
168
|
}
|
package/cli/lib/fs-helpers.mjs
CHANGED
|
@@ -1,33 +1,157 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { lstat, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const SHARED_INODE_FORBIDDEN_PATHS = new Set([
|
|
5
|
+
".nimi/methodology/authority-authoring.yaml",
|
|
6
|
+
"AGENTS.md",
|
|
7
|
+
"CLAUDE.md",
|
|
8
|
+
".gitignore",
|
|
9
|
+
".gitattributes",
|
|
10
|
+
]);
|
|
11
|
+
const FATAL_UTF8_HOST_PATHS = new Set(["AGENTS.md", "CLAUDE.md", ".gitignore", ".gitattributes"]);
|
|
12
|
+
|
|
13
|
+
const BASE_MANAGED_PATHS = [
|
|
14
|
+
[".nimi", "directory"],
|
|
15
|
+
[".nimi/local", "directory"],
|
|
16
|
+
[".nimi/methodology", "directory"],
|
|
17
|
+
[".nimi/methodology/authority-authoring.yaml", "file"],
|
|
18
|
+
[".nimi/config", "directory"],
|
|
19
|
+
[".nimi/config/spec-generation-inputs.yaml", "file"],
|
|
20
|
+
[".nimi/contracts", "directory"],
|
|
21
|
+
[".nimi/contracts/domain-admission.schema.yaml", "file"],
|
|
22
|
+
["AGENTS.md", "file"],
|
|
23
|
+
["CLAUDE.md", "file"],
|
|
24
|
+
[".gitignore", "file"],
|
|
25
|
+
[".gitattributes", "file"],
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export class ManagedPathError extends Error {
|
|
29
|
+
constructor(message) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "ManagedPathError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class ManagedTextError extends Error {
|
|
36
|
+
constructor(message) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = "ManagedTextError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
2
41
|
|
|
3
42
|
export async function pathExists(targetPath) {
|
|
4
43
|
try {
|
|
5
|
-
return await
|
|
44
|
+
return await lstat(targetPath);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error?.code === "ENOENT") return null;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function portable(value) {
|
|
52
|
+
return value.split(path.sep).join(path.posix.sep);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function inspectManagedPath(projectRoot, relativePath, expectedType) {
|
|
56
|
+
const components = relativePath.split("/");
|
|
57
|
+
let current = projectRoot;
|
|
58
|
+
for (let index = 0; index < components.length; index += 1) {
|
|
59
|
+
current = path.join(current, components[index]);
|
|
60
|
+
let info;
|
|
61
|
+
try {
|
|
62
|
+
info = await lstat(current);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error?.code === "ENOENT") return;
|
|
65
|
+
throw new ManagedPathError(`cannot inspect managed path ${portable(path.relative(projectRoot, current))}: ${error.message}`);
|
|
66
|
+
}
|
|
67
|
+
const ref = portable(path.relative(projectRoot, current));
|
|
68
|
+
if (info.isSymbolicLink()) throw new ManagedPathError(`managed path boundary refuses symbolic link: ${ref}`);
|
|
69
|
+
const isTarget = index === components.length - 1;
|
|
70
|
+
if (!isTarget && !info.isDirectory()) throw new ManagedPathError(`managed path ancestor must be a directory: ${ref}`);
|
|
71
|
+
if (isTarget && expectedType === "directory" && !info.isDirectory()) throw new ManagedPathError(`managed path must be a directory: ${ref}`);
|
|
72
|
+
if (isTarget && expectedType === "file" && !info.isFile()) throw new ManagedPathError(`managed path must be a regular file: ${ref}`);
|
|
73
|
+
if (isTarget && info.isFile() && SHARED_INODE_FORBIDDEN_PATHS.has(relativePath) && info.nlink > 1) {
|
|
74
|
+
throw new ManagedPathError(`managed regular file must not share an inode: ${ref}`);
|
|
75
|
+
}
|
|
76
|
+
if (isTarget && info.isFile() && FATAL_UTF8_HOST_PATHS.has(relativePath)) await readUtf8FileFatal(current, ref);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function preflightManagedProjectPaths(projectRoot, options = {}) {
|
|
81
|
+
const absoluteRoot = path.resolve(projectRoot);
|
|
82
|
+
let rootInfo;
|
|
83
|
+
try {
|
|
84
|
+
rootInfo = await lstat(absoluteRoot);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new ManagedPathError(`cannot inspect project root: ${error.message}`);
|
|
87
|
+
}
|
|
88
|
+
if (rootInfo.isSymbolicLink()) throw new ManagedPathError("managed path boundary refuses symbolic-link project root");
|
|
89
|
+
if (!rootInfo.isDirectory()) throw new ManagedPathError("project root must be a directory");
|
|
90
|
+
const specs = options.includeSpec
|
|
91
|
+
? [...BASE_MANAGED_PATHS, [".nimi/spec", "directory"]]
|
|
92
|
+
: BASE_MANAGED_PATHS;
|
|
93
|
+
for (const [relativePath, expectedType] of specs) {
|
|
94
|
+
await inspectManagedPath(absoluteRoot, relativePath, expectedType);
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function readUtf8FileFatal(filePath, label = filePath) {
|
|
100
|
+
const bytes = await readFile(filePath);
|
|
101
|
+
try {
|
|
102
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
6
103
|
} catch {
|
|
7
|
-
|
|
104
|
+
throw new ManagedTextError(`managed host text is not valid UTF-8: ${label}`);
|
|
8
105
|
}
|
|
9
106
|
}
|
|
10
107
|
|
|
11
108
|
export async function readTextIfFile(filePath) {
|
|
12
109
|
const info = await pathExists(filePath);
|
|
13
|
-
if (!info || !info.isFile())
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
|
|
110
|
+
if (!info || !info.isFile() || info.isSymbolicLink()) return null;
|
|
17
111
|
return readFile(filePath, "utf8");
|
|
18
112
|
}
|
|
19
113
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
: "";
|
|
24
|
-
const missing = entries.filter((entry) => !existing.includes(entry));
|
|
114
|
+
export function normalizeTextToLf(text) {
|
|
115
|
+
return String(text).replace(/\r\n?/gu, "\n");
|
|
116
|
+
}
|
|
25
117
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
118
|
+
function exactTextLines(text) {
|
|
119
|
+
return String(text ?? "")
|
|
120
|
+
.split(/\n/u)
|
|
121
|
+
.map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function hasExactTextLine(text, entry) {
|
|
125
|
+
return exactTextLines(text).includes(entry);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function effectiveGitignoreRuleLines(text) {
|
|
129
|
+
return String(text ?? "")
|
|
130
|
+
.split(/\n/u)
|
|
131
|
+
.map((line) => line.endsWith("\r") ? line.slice(0, -1) : line)
|
|
132
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function hasExactGitignoreRule(text, entry) {
|
|
136
|
+
return effectiveGitignoreRuleLines(text).at(-1) === entry;
|
|
137
|
+
}
|
|
29
138
|
|
|
139
|
+
export async function appendGitignoreEntries(gitignorePath, entries) {
|
|
140
|
+
const existingInfo = await pathExists(gitignorePath);
|
|
141
|
+
const existing = existingInfo?.isFile() ? await readUtf8FileFatal(gitignorePath, ".gitignore") : "";
|
|
142
|
+
const missing = entries.filter((entry) => !hasExactGitignoreRule(existing, entry));
|
|
143
|
+
if (missing.length === 0) return false;
|
|
30
144
|
const prefix = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
|
|
31
145
|
await writeFile(gitignorePath, `${existing}${prefix}${missing.join("\n")}\n`, "utf8");
|
|
32
146
|
return true;
|
|
33
147
|
}
|
|
148
|
+
|
|
149
|
+
export async function appendGitattributesEntries(gitattributesPath, entries) {
|
|
150
|
+
const existingInfo = await pathExists(gitattributesPath);
|
|
151
|
+
const existing = existingInfo?.isFile() ? await readUtf8FileFatal(gitattributesPath, ".gitattributes") : "";
|
|
152
|
+
const missing = entries.filter((entry) => !hasExactTextLine(existing, entry));
|
|
153
|
+
if (missing.length === 0) return false;
|
|
154
|
+
const prefix = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
|
|
155
|
+
await writeFile(gitattributesPath, `${existing}${prefix}${missing.join("\n")}\n`, "utf8");
|
|
156
|
+
return true;
|
|
157
|
+
}
|