@nimiplatform/nimi-coding 0.4.0 → 0.6.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 +16 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +92 -55
- package/README.zh-CN.md +93 -56
- package/cli/commands/authority.mjs +80 -125
- package/cli/commands/clear.mjs +3 -2
- package/cli/commands/code.mjs +225 -0
- package/cli/commands/start.mjs +3 -2
- package/cli/constants.mjs +2 -1
- package/cli/dev/check-authority-surface.mjs +17 -8
- package/cli/dev/check-product-conformance.mjs +4 -32
- package/cli/dev/run-cli-smoke.mjs +6 -0
- package/cli/help.mjs +8 -3
- package/cli/index.mjs +3 -2
- package/cli/lib/authority/audit.mjs +262 -185
- package/cli/lib/authority/change-candidates.mjs +807 -0
- package/cli/lib/authority/closed-sets.mjs +7 -8
- package/cli/lib/authority/compile.mjs +6 -24
- package/cli/lib/authority/compiled-snapshot.mjs +143 -0
- package/cli/lib/authority/consumers.mjs +69 -19
- package/cli/lib/authority/diagnostics.mjs +10 -2
- package/cli/lib/authority/diff.mjs +77 -54
- package/cli/lib/authority/format.mjs +4 -32
- package/cli/lib/authority/git-snapshot.mjs +34 -379
- package/cli/lib/authority/graph.mjs +10 -10
- package/cli/lib/authority/impact.mjs +17 -144
- package/cli/lib/authority/review.mjs +33 -43
- package/cli/lib/authority/sarif.mjs +1 -8
- package/cli/lib/authority/source-yaml.mjs +0 -4
- package/cli/lib/authority/surface.mjs +19 -1
- package/cli/lib/authority/terms.mjs +10 -9
- package/cli/lib/authority/validate.mjs +6 -14
- package/cli/lib/code/authority-annotation-response.mjs +43 -0
- package/cli/lib/code/authority-annotation-scan.mjs +162 -0
- package/cli/lib/code/authority-annotations.mjs +293 -0
- package/cli/lib/code/context.mjs +710 -0
- package/cli/lib/code/response.mjs +165 -0
- package/cli/lib/doctor.mjs +3 -2
- package/cli/lib/entrypoints.mjs +13 -5
- package/cli/seeds/seed-policy.yaml +2 -2
- package/contracts/authority-source.schema.yaml +0 -6
- package/contracts/authority-verifier-bindings.schema.yaml +21 -10
- package/methodology/authority-authoring.yaml +68 -70
- package/methodology/core.yaml +10 -6
- package/package.json +5 -3
- package/spec/authority-audit.yaml +56 -52
- package/spec/authority-authoring.yaml +3 -16
- package/spec/authority-change-candidates.yaml +146 -0
- package/spec/authority-consumers.yaml +7 -0
- package/spec/authority-discovery.yaml +2 -2
- package/spec/authority-graph.yaml +1 -1
- package/spec/authority-impact.yaml +16 -39
- package/spec/authority-review.yaml +29 -47
- package/spec/authority-terms.yaml +5 -1
- package/spec/code-authority-annotations.yaml +42 -0
- package/spec/code-context.yaml +38 -0
- package/spec/product-scope.yaml +15 -9
- package/cli/commands/validate-ai-governance.mjs +0 -335
- package/cli/lib/authority/evidence-snapshot.mjs +0 -1
- package/cli/lib/authority/evidence.mjs +0 -1432
- package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +0 -377
- package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +0 -376
- package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +0 -155
- package/cli/lib/internal/governance/ai/check-high-risk-doc-metadata-core.mjs +0 -173
- package/cli/lib/internal/governance/config.mjs +0 -114
- package/cli/lib/internal/governance/shared/read-yaml-with-fragments.mjs +0 -49
- package/contracts/authority-evidence-bindings.schema.yaml +0 -53
- package/contracts/authority-evidence-probe-results.schema.yaml +0 -38
- package/contracts/authority-impact-dispositions.schema.yaml +0 -13
- package/spec/authority-evidence.yaml +0 -200
|
@@ -65,11 +65,6 @@ function refused(diagnostics, { fileCount = 0 } = {}) {
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function isWithin(root, candidate) {
|
|
69
|
-
const relative = path.relative(root, candidate);
|
|
70
|
-
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
68
|
function portableRelative(repository, candidate) {
|
|
74
69
|
return path.relative(repository, candidate).split(path.sep).join(path.posix.sep);
|
|
75
70
|
}
|
|
@@ -540,12 +535,16 @@ export async function closedSetsAuthorityRepository(repositoryPath, specPath, {
|
|
|
540
535
|
}
|
|
541
536
|
const canonical = canonicalGroups(compiled);
|
|
542
537
|
|
|
538
|
+
const gitlinks = new Set(inventory.gitlinks);
|
|
543
539
|
const classifiedFiles = await classifyTrackedTextFiles(
|
|
544
540
|
inventory.repository,
|
|
545
541
|
inventory.files,
|
|
546
|
-
(file) =>
|
|
547
|
-
|
|
548
|
-
|
|
542
|
+
(file) => {
|
|
543
|
+
if (gitlinks.has(file)) return "gitlink";
|
|
544
|
+
return file === ".nimi/spec" || file.startsWith(".nimi/spec/")
|
|
545
|
+
? "authority-corpus"
|
|
546
|
+
: implementationFileKind(file) === null ? "not-a-scanned-kind" : null;
|
|
547
|
+
},
|
|
549
548
|
);
|
|
550
549
|
if (classifiedFiles.error !== undefined) {
|
|
551
550
|
return refused([inputDiagnostic("AUTH_CLOSED_SETS_IMPLEMENTATION_INPUT_INVALID", classifiedFiles.error)], { fileCount: compiled.fileCount });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { compareText
|
|
1
|
+
import { compareText } from "./diagnostics.mjs";
|
|
2
2
|
import { formattingDiagnostics, parseAuthorityPath } from "./format.mjs";
|
|
3
3
|
import { validateAuthorityScopeBindings } from "./scope-bindings.mjs";
|
|
4
4
|
import { buildSourceMap } from "./source-map.mjs";
|
|
@@ -39,31 +39,13 @@ async function admittedSources(inputPath) {
|
|
|
39
39
|
return { ok: true, diagnostics: [], parsed };
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function
|
|
43
|
-
if (requiredFormat === null) return [];
|
|
44
|
-
return sortDiagnostics(documents.flatMap((document) => {
|
|
45
|
-
const found = document.data?.format;
|
|
46
|
-
if (found === requiredFormat) return [];
|
|
47
|
-
return [makeDiagnostic({
|
|
48
|
-
code: "AUTH_FORMAT_REQUIRED",
|
|
49
|
-
file: document.file,
|
|
50
|
-
range: document.locations.get("/format") ?? document.locations.get("") ?? document.locator.range(0),
|
|
51
|
-
pointer: "/format",
|
|
52
|
-
reason: `authority check requires format ${requiredFormat}, but ${document.file} declares ${found}`,
|
|
53
|
-
repair: "rewrite the container in the explicitly required admitted format without changing product semantics",
|
|
54
|
-
})];
|
|
55
|
-
}));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export async function checkAuthorityPath(inputPath, { scopeBindings = null, requireFormat = null } = {}) {
|
|
42
|
+
export async function checkAuthorityPath(inputPath, { scopeBindings = null } = {}) {
|
|
59
43
|
const result = await admittedSources(inputPath);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
ok: result.ok && formatDiagnostics.length === 0,
|
|
64
|
-
diagnostics,
|
|
44
|
+
if (!result.ok || scopeBindings === null) return {
|
|
45
|
+
ok: result.ok,
|
|
46
|
+
diagnostics: result.diagnostics,
|
|
65
47
|
fileCount: result.parsed.files.length,
|
|
66
|
-
unitCount: result.ok
|
|
48
|
+
unitCount: result.ok ? result.parsed.sources.length : 0,
|
|
67
49
|
};
|
|
68
50
|
const scopeValidation = await validateAuthorityScopeBindings(result.parsed.sources, scopeBindings);
|
|
69
51
|
return {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { compareText, portablePath } from "./diagnostics.mjs";
|
|
4
|
+
|
|
5
|
+
class ReadonlyIndex {
|
|
6
|
+
#values;
|
|
7
|
+
|
|
8
|
+
constructor(entries) {
|
|
9
|
+
this.#values = new Map(entries);
|
|
10
|
+
Object.freeze(this);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get size() {
|
|
14
|
+
return this.#values.size;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get(key) {
|
|
18
|
+
return this.#values.get(key);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
has(key) {
|
|
22
|
+
return this.#values.has(key);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
*entries() {
|
|
26
|
+
for (const [key, value] of this.#values) yield Object.freeze([key, value]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
keys() {
|
|
30
|
+
return this.#values.keys();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
values() {
|
|
34
|
+
return this.#values.values();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
forEach(callback, thisArg = undefined) {
|
|
38
|
+
for (const [key, value] of this.#values) callback.call(thisArg, value, key, this);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
[Symbol.iterator]() {
|
|
42
|
+
return this.entries();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get [Symbol.toStringTag]() {
|
|
46
|
+
return "ReadonlyIndex";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function freezeTree(value, seen = new Set()) {
|
|
51
|
+
if (value === null || typeof value !== "object" || Object.isFrozen(value) || seen.has(value)) return value;
|
|
52
|
+
seen.add(value);
|
|
53
|
+
for (const key of Reflect.ownKeys(value)) freezeTree(value[key], seen);
|
|
54
|
+
return Object.freeze(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readonlyIndex(entries) {
|
|
58
|
+
return new ReadonlyIndex([...entries].sort(([left], [right]) => compareText(left, right)));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function groupedIndex(entries, keys) {
|
|
62
|
+
const groups = new Map();
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
for (const key of keys(entry)) {
|
|
65
|
+
const values = groups.get(key) ?? [];
|
|
66
|
+
values.push(entry);
|
|
67
|
+
groups.set(key, values);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return readonlyIndex([...groups.entries()].map(([key, values]) => [
|
|
71
|
+
key,
|
|
72
|
+
Object.freeze(values.sort((left, right) => compareText(left.unit.id, right.unit.id))),
|
|
73
|
+
]));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function compareEdge(left, right) {
|
|
77
|
+
return compareText(left.source, right.source)
|
|
78
|
+
|| compareText(left.type, right.type)
|
|
79
|
+
|| compareText(left.target, right.target);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function adjacencyIndexes(entries) {
|
|
83
|
+
const outgoing = new Map(entries.map((entry) => [entry.unit.id, []]));
|
|
84
|
+
const incoming = new Map(entries.map((entry) => [entry.unit.id, []]));
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
entry.unit.relations.forEach((relation, relationIndex) => {
|
|
87
|
+
const edge = Object.freeze({
|
|
88
|
+
source: entry.unit.id,
|
|
89
|
+
type: relation.type,
|
|
90
|
+
target: relation.target,
|
|
91
|
+
sourceUnitIndex: entry.unitIndex,
|
|
92
|
+
relationIndex,
|
|
93
|
+
});
|
|
94
|
+
outgoing.get(edge.source).push(edge);
|
|
95
|
+
incoming.get(edge.target).push(edge);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const finish = (index) => readonlyIndex([...index.entries()].map(([id, edges]) => [
|
|
99
|
+
id,
|
|
100
|
+
Object.freeze(edges.sort(compareEdge)),
|
|
101
|
+
]));
|
|
102
|
+
return { outgoing: finish(outgoing), incoming: finish(incoming) };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createCompiledAuthoritySnapshot(compiled, inputRoot) {
|
|
106
|
+
if (
|
|
107
|
+
compiled?.ok !== true
|
|
108
|
+
|| !Array.isArray(compiled?.ir?.units)
|
|
109
|
+
|| compiled?.sourceMap?.fields === null
|
|
110
|
+
|| typeof compiled?.sourceMap?.fields !== "object"
|
|
111
|
+
) {
|
|
112
|
+
throw new TypeError("compiled authority snapshot requires one successful compiler result");
|
|
113
|
+
}
|
|
114
|
+
if (typeof inputRoot !== "string" || inputRoot.length === 0) {
|
|
115
|
+
throw new TypeError("compiled authority snapshot requires one non-empty input root");
|
|
116
|
+
}
|
|
117
|
+
const resolvedRoot = path.resolve(inputRoot);
|
|
118
|
+
const entries = compiled.ir.units.map((unit, unitIndex) => {
|
|
119
|
+
const mapped = compiled.sourceMap.fields[`/units/${unitIndex}/id`];
|
|
120
|
+
if (!mapped) throw new TypeError(`compiled authority snapshot cannot locate unit identity: ${unit.id}`);
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
unit,
|
|
123
|
+
unitIndex,
|
|
124
|
+
container: portablePath(mapped.file, resolvedRoot),
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
const byId = readonlyIndex(entries.map((entry) => [entry.unit.id, entry]));
|
|
128
|
+
const byOwner = groupedIndex(entries, (entry) => [entry.unit.owner]);
|
|
129
|
+
const byScope = groupedIndex(entries, (entry) => entry.unit.semantic?.scope ?? []);
|
|
130
|
+
const byContainer = groupedIndex(entries, (entry) => [entry.container]);
|
|
131
|
+
const { incoming, outgoing } = adjacencyIndexes(entries);
|
|
132
|
+
freezeTree(compiled);
|
|
133
|
+
return Object.freeze({
|
|
134
|
+
inputRoot: resolvedRoot,
|
|
135
|
+
compiled,
|
|
136
|
+
byId,
|
|
137
|
+
byOwner,
|
|
138
|
+
byScope,
|
|
139
|
+
byContainer,
|
|
140
|
+
incoming,
|
|
141
|
+
outgoing,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import YAML from "yaml";
|
|
5
5
|
|
|
6
|
-
import { compareText, makeDiagnostic, sortDiagnostics } from "./diagnostics.mjs";
|
|
6
|
+
import { compareText, isPathWithin, makeDiagnostic, sortDiagnostics } from "./diagnostics.mjs";
|
|
7
7
|
import { AuthorityReviewRefusal } from "./git-snapshot.mjs";
|
|
8
8
|
import { resolveRepositoryCorpus } from "./repository-corpus.mjs";
|
|
9
9
|
import { repositoryInventory } from "./repository-inventory.mjs";
|
|
@@ -15,7 +15,10 @@ const CONSUMERS_FORMAT = "nimicoding.authority-consumers/v1";
|
|
|
15
15
|
// requiring a trailing segment reported it as no reference at all.
|
|
16
16
|
// A separator run rather than one character: inside a source string literal a
|
|
17
17
|
// Windows path is written escaped, so the file text holds ".nimi\\spec".
|
|
18
|
-
|
|
18
|
+
// A single backslash followed by a common source-string escape starter ends a
|
|
19
|
+
// local path instead of creating another path segment. Escaped Windows
|
|
20
|
+
// separators remain visible in source as a run of at least two backslashes.
|
|
21
|
+
const SPEC_REFERENCE = /\.nimi(?:\/+|\\{2,}|\\(?![nrtbfv0xu'"`$]))spec(?:(?:\/+|\\{2,}|\\(?![nrtbfv0xu'"`$]))[A-Za-z0-9_.*<>-]+)*/gu;
|
|
19
22
|
// A path built from adjacent string literals, e.g. path.join(root, '.nimi',
|
|
20
23
|
// 'spec', 'runtime', 'x.yaml'). Reconstructed only while every segment is a
|
|
21
24
|
// literal; the first non-literal segment makes the target unresolved.
|
|
@@ -84,11 +87,6 @@ function repositoryDiagnostic(error) {
|
|
|
84
87
|
return inputDiagnostic("AUTH_CONSUMERS_REPOSITORY_INVALID", error.message, error.file ?? ".");
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
function isWithin(root, candidate) {
|
|
88
|
-
const relative = path.relative(root, candidate);
|
|
89
|
-
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
90
|
async function resolveCorpus(repository, specPath) {
|
|
93
91
|
return resolveRepositoryCorpus(repository, specPath, "authority consumer");
|
|
94
92
|
}
|
|
@@ -115,29 +113,66 @@ function lineCounter(text) {
|
|
|
115
113
|
};
|
|
116
114
|
}
|
|
117
115
|
|
|
116
|
+
function collapseEscapedLineContinuations(text) {
|
|
117
|
+
const chunks = [];
|
|
118
|
+
const continuations = [];
|
|
119
|
+
let segmentStart = 0;
|
|
120
|
+
let searchFrom = 0;
|
|
121
|
+
let normalizedLength = 0;
|
|
122
|
+
for (let index = text.indexOf("\\", searchFrom); index !== -1; index = text.indexOf("\\", searchFrom)) {
|
|
123
|
+
const width = text[index + 1] === "\n"
|
|
124
|
+
? 2
|
|
125
|
+
: text[index + 1] === "\r" && text[index + 2] === "\n" ? 3 : 0;
|
|
126
|
+
if (width === 0) {
|
|
127
|
+
searchFrom = index + 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const chunk = text.slice(segmentStart, index);
|
|
131
|
+
chunks.push(chunk);
|
|
132
|
+
normalizedLength += chunk.length;
|
|
133
|
+
continuations.push({ offset: normalizedLength, width });
|
|
134
|
+
segmentStart = index + width;
|
|
135
|
+
searchFrom = segmentStart;
|
|
136
|
+
}
|
|
137
|
+
if (continuations.length === 0) return { text, originalOffset: (index) => index };
|
|
138
|
+
chunks.push(text.slice(segmentStart));
|
|
139
|
+
return {
|
|
140
|
+
text: chunks.join(""),
|
|
141
|
+
originalOffset(index) {
|
|
142
|
+
let removedWidth = 0;
|
|
143
|
+
for (const continuation of continuations) {
|
|
144
|
+
if (continuation.offset > index) break;
|
|
145
|
+
removedWidth += continuation.width;
|
|
146
|
+
}
|
|
147
|
+
return index + removedWidth;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
118
152
|
function extractReferences(consumer, text) {
|
|
153
|
+
const logical = collapseEscapedLineContinuations(text);
|
|
119
154
|
const found = [];
|
|
120
|
-
for (const match of text.matchAll(SPEC_REFERENCE)) {
|
|
155
|
+
for (const match of logical.text.matchAll(SPEC_REFERENCE)) {
|
|
121
156
|
const target = match[0].replace(/[/\\]+/gu, "/");
|
|
122
157
|
if (target.includes("*") || target.includes("<") || target.includes(">")) continue;
|
|
123
|
-
found.push({ index: match.index, target, reason: null });
|
|
158
|
+
found.push({ index: logical.originalOffset(match.index), target, reason: null });
|
|
124
159
|
}
|
|
125
|
-
for (const match of text.matchAll(SPEC_CONSTRUCTOR)) {
|
|
160
|
+
for (const match of logical.text.matchAll(SPEC_CONSTRUCTOR)) {
|
|
126
161
|
const segments = [...match[3].matchAll(CONSTRUCTOR_SEGMENT)].map((segment) => segment[2]);
|
|
127
162
|
const target = [".nimi", "spec", ...segments].join("/");
|
|
128
163
|
// A further segment that is not a string literal makes the built path
|
|
129
164
|
// unknowable here. Reporting it as unresolved keeps the scan from
|
|
130
165
|
// presenting silence as proof that nothing consumes the corpus.
|
|
131
|
-
const continues = CONSTRUCTOR_CONTINUES.test(text.slice(match.index + match[0].length));
|
|
132
|
-
found.push({ index: match.index, target, reason: continues ? "non-literal-path-segment" : null });
|
|
166
|
+
const continues = CONSTRUCTOR_CONTINUES.test(logical.text.slice(match.index + match[0].length));
|
|
167
|
+
found.push({ index: logical.originalOffset(match.index), target, reason: continues ? "non-literal-path-segment" : null });
|
|
133
168
|
}
|
|
134
|
-
for (const match of text.matchAll(EXTERNAL_REFERENCE)) {
|
|
169
|
+
for (const match of logical.text.matchAll(EXTERNAL_REFERENCE)) {
|
|
135
170
|
const externalPath = match[2];
|
|
136
171
|
const fragment = match[3] ?? null;
|
|
137
172
|
const coordinate = `${match[1]}:${externalPath}${fragment === null ? "" : `#${fragment}`}`;
|
|
138
173
|
const nonCanonical = externalPath.split("/").some((segment) => segment === "." || segment === "..");
|
|
139
174
|
found.push({
|
|
140
|
-
index: match.index + match[0].indexOf(`${match[1]}:`),
|
|
175
|
+
index: logical.originalOffset(match.index + match[0].indexOf(`${match[1]}:`)),
|
|
141
176
|
target: coordinate,
|
|
142
177
|
scheme: match[1],
|
|
143
178
|
path: externalPath,
|
|
@@ -173,18 +208,28 @@ function targetExists(target, trackedFiles) {
|
|
|
173
208
|
// own. Without a root for that scheme the target is unknowable here, which is
|
|
174
209
|
// not the same claim as dead.
|
|
175
210
|
function classifyLocalReference(reference, trackedFiles) {
|
|
176
|
-
|
|
211
|
+
if (targetExists(reference.target, trackedFiles)) return { classification: "canonical", reference };
|
|
212
|
+
if (reference.target.endsWith(".")) {
|
|
213
|
+
const withoutSentencePeriod = reference.target.slice(0, -1);
|
|
214
|
+
if (targetExists(withoutSentencePeriod, trackedFiles)) {
|
|
215
|
+
return {
|
|
216
|
+
classification: "canonical",
|
|
217
|
+
reference: { ...reference, target: withoutSentencePeriod },
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { classification: "canonicalDead", reference };
|
|
177
222
|
}
|
|
178
223
|
|
|
179
224
|
async function externalTargetStatus(root, relative) {
|
|
180
225
|
const candidate = path.resolve(root, ...relative.split("/"));
|
|
181
|
-
if (!
|
|
226
|
+
if (!isPathWithin(root, candidate)) return "non-canonical";
|
|
182
227
|
|
|
183
228
|
let probe = candidate;
|
|
184
229
|
while (true) {
|
|
185
230
|
try {
|
|
186
231
|
const resolved = await realpath(probe);
|
|
187
|
-
if (!
|
|
232
|
+
if (!isPathWithin(root, resolved)) return "non-canonical";
|
|
188
233
|
return probe === candidate ? "authority" : "dead";
|
|
189
234
|
} catch (error) {
|
|
190
235
|
if (error.code !== "ENOENT" && error.code !== "ENOTDIR") throw error;
|
|
@@ -354,10 +399,14 @@ export async function consumersAuthorityRepository(repositoryPath, specPath, { m
|
|
|
354
399
|
}
|
|
355
400
|
|
|
356
401
|
const candidates = [...new Set([...inventory.files, "package.json"])].sort(compareText);
|
|
402
|
+
const gitlinks = new Set(inventory.gitlinks);
|
|
357
403
|
const classifiedFiles = await classifyTrackedTextFiles(
|
|
358
404
|
inventory.repository,
|
|
359
405
|
candidates,
|
|
360
|
-
(file) =>
|
|
406
|
+
(file) => {
|
|
407
|
+
if (gitlinks.has(file)) return "gitlink";
|
|
408
|
+
return isSpecSource(file) ? "authority-corpus" : null;
|
|
409
|
+
},
|
|
361
410
|
);
|
|
362
411
|
if (classifiedFiles.error !== undefined) {
|
|
363
412
|
return refused([inputDiagnostic("AUTH_CONSUMERS_INPUT_INVALID", classifiedFiles.error)]);
|
|
@@ -401,7 +450,8 @@ export async function consumersAuthorityRepository(repositoryPath, specPath, { m
|
|
|
401
450
|
try {
|
|
402
451
|
for (const reference of references) {
|
|
403
452
|
if (reference.external === undefined) {
|
|
404
|
-
|
|
453
|
+
const local = classifyLocalReference(reference, trackedFiles);
|
|
454
|
+
classified[local.classification].push(local.reference);
|
|
405
455
|
continue;
|
|
406
456
|
}
|
|
407
457
|
const root = roots.get(reference.external.scheme);
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
+
import { PROJECT_LOCAL_CLI_INVOCATION } from "../../constants.mjs";
|
|
4
|
+
|
|
3
5
|
export function compareText(left, right) {
|
|
4
6
|
const a = String(left);
|
|
5
7
|
const b = String(right);
|
|
6
8
|
return a === b ? 0 : a < b ? -1 : 1;
|
|
7
9
|
}
|
|
8
10
|
|
|
11
|
+
export function isPathWithin(root, candidate) {
|
|
12
|
+
const relative = path.relative(root, candidate);
|
|
13
|
+
return relative === ""
|
|
14
|
+
|| (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
export function portablePath(filePath, cwd = process.cwd()) {
|
|
10
18
|
const relative = path.relative(cwd, filePath);
|
|
11
|
-
const value = relative &&
|
|
19
|
+
const value = relative && isPathWithin(cwd, filePath) ? relative : filePath;
|
|
12
20
|
return value.split(path.sep).join(path.posix.sep);
|
|
13
21
|
}
|
|
14
22
|
|
|
@@ -83,5 +91,5 @@ export const REPAIRS = {
|
|
|
83
91
|
invalid: "replace the value with one admitted by the authoring contract",
|
|
84
92
|
relation: "repair the explicit relation using product authority; do not infer a target",
|
|
85
93
|
duplicate: "keep one declaration chosen from product authority and remove the duplicate",
|
|
86
|
-
format:
|
|
94
|
+
format: `run \`${PROJECT_LOCAL_CLI_INVOCATION} authority fmt\` on this file`,
|
|
87
95
|
};
|