unknown-knowledge 2.1.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/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +167 -0
- package/cli/.gitkeep +0 -0
- package/cli/commands/init-copy.js +90 -0
- package/cli/commands/init.js +386 -0
- package/cli/init-copy.js +24 -0
- package/cli/init.js +24 -0
- package/cli/kit.manifest.yaml +256 -0
- package/cli/lib/copy-payload.js +334 -0
- package/cli/lib/generate-wrappers.js +162 -0
- package/package.json +36 -0
- package/payload/adapter-fixtures/README.md +53 -0
- package/payload/adapter-fixtures/html/EXPECTED.yaml +50 -0
- package/payload/adapter-fixtures/html/sample.html +38 -0
- package/payload/adapter-fixtures/md/EXPECTED.yaml +65 -0
- package/payload/adapter-fixtures/md/sample.md +32 -0
- package/payload/adapter-fixtures/pdf/EXPECTED.yaml +45 -0
- package/payload/adapter-fixtures/pdf/sample.pdf +0 -0
- package/payload/adapter-fixtures/txt/EXPECTED.yaml +31 -0
- package/payload/adapter-fixtures/txt/sample.txt +18 -0
- package/payload/docs/README.md +102 -0
- package/payload/docs/boundaries.md +60 -0
- package/payload/docs/ci-wiring.md +109 -0
- package/payload/docs/steward-guide.md +238 -0
- package/payload/engine/audit.js +34 -0
- package/payload/engine/commands/audit.js +289 -0
- package/payload/engine/commands/derive.js +334 -0
- package/payload/engine/commands/ingest.js +124 -0
- package/payload/engine/commands/log-entry.js +85 -0
- package/payload/engine/commands/phoenix.js +206 -0
- package/payload/engine/commands/preflight.js +530 -0
- package/payload/engine/commands/resolve.js +1678 -0
- package/payload/engine/commands/survey-map.js +351 -0
- package/payload/engine/commands/validate-values.js +315 -0
- package/payload/engine/commands/validate.js +1426 -0
- package/payload/engine/derive.js +34 -0
- package/payload/engine/ingest.js +34 -0
- package/payload/engine/lib/anchor-signatures.js +126 -0
- package/payload/engine/lib/boot.js +39 -0
- package/payload/engine/lib/call-numbers.js +133 -0
- package/payload/engine/lib/cli.js +147 -0
- package/payload/engine/lib/coverage.js +849 -0
- package/payload/engine/lib/decomposition.js +225 -0
- package/payload/engine/lib/derived.js +494 -0
- package/payload/engine/lib/engine-refusal.js +40 -0
- package/payload/engine/lib/exit-codes.js +14 -0
- package/payload/engine/lib/extractor-kinds.js +955 -0
- package/payload/engine/lib/format-adapters.js +802 -0
- package/payload/engine/lib/id-grammars.js +178 -0
- package/payload/engine/lib/iso-date.js +55 -0
- package/payload/engine/lib/kit-root.js +101 -0
- package/payload/engine/lib/load-stores.js +1624 -0
- package/payload/engine/lib/log-entry.js +196 -0
- package/payload/engine/lib/phoenix.js +628 -0
- package/payload/engine/lib/scoring.js +150 -0
- package/payload/engine/lib/suppressions.js +172 -0
- package/payload/engine/lib/time-verdicts.js +282 -0
- package/payload/engine/lib/usage-error.js +14 -0
- package/payload/engine/lib/validate-record.js +504 -0
- package/payload/engine/log-entry.js +34 -0
- package/payload/engine/phoenix.js +39 -0
- package/payload/engine/preflight.js +34 -0
- package/payload/engine/resolve.js +34 -0
- package/payload/engine/survey-map.js +34 -0
- package/payload/engine/validate-values.js +34 -0
- package/payload/engine/validate.js +34 -0
- package/payload/extractor-fixtures/.gitkeep +0 -0
- package/payload/extractor-fixtures/README.md +29 -0
- package/payload/extractor-fixtures/swift/strings-keys/EXPECTED.yaml +8 -0
- package/payload/extractor-fixtures/swift/strings-keys/sample.strings +15 -0
- package/payload/extractor-fixtures/swift/swift-const-array/EXPECTED.yaml +7 -0
- package/payload/extractor-fixtures/swift/swift-const-array/sample.swift +21 -0
- package/payload/extractor-fixtures/swift/swift-enum/EXPECTED.yaml +8 -0
- package/payload/extractor-fixtures/swift/swift-enum/sample.swift +30 -0
- package/payload/extractor-fixtures/swift/yaml-keys/EXPECTED.yaml +6 -0
- package/payload/extractor-fixtures/swift/yaml-keys/sample.yaml +23 -0
- package/payload/extractor-fixtures/swift/yaml-map-keys/EXPECTED.yaml +7 -0
- package/payload/extractor-fixtures/swift/yaml-map-keys/sample.yaml +15 -0
- package/payload/extractor-fixtures/ts/dir-modules/EXPECTED.yaml +12 -0
- package/payload/extractor-fixtures/ts/dir-modules/sample-modules/alpha.widget.ts +1 -0
- package/payload/extractor-fixtures/ts/dir-modules/sample-modules/beta.widget.ts +1 -0
- package/payload/extractor-fixtures/ts/dir-modules/sample-modules/gamma.widget.ts +1 -0
- package/payload/extractor-fixtures/ts/dir-modules/sample-modules/helpers/format.ts +3 -0
- package/payload/extractor-fixtures/ts/dir-modules/sample-modules/widgets.test.ts +2 -0
- package/payload/extractor-fixtures/ts/json-keys/EXPECTED.yaml +7 -0
- package/payload/extractor-fixtures/ts/json-keys/sample.json +5 -0
- package/payload/extractor-fixtures/ts/json-map-keys/EXPECTED.yaml +7 -0
- package/payload/extractor-fixtures/ts/json-map-keys/sample.json +13 -0
- package/payload/extractor-fixtures/ts/ts-const-array/EXPECTED.yaml +6 -0
- package/payload/extractor-fixtures/ts/ts-const-array/sample.ts +13 -0
- package/payload/extractor-fixtures/ts/ts-enum/EXPECTED.yaml +7 -0
- package/payload/extractor-fixtures/ts/ts-enum/sample.ts +11 -0
- package/payload/extractor-fixtures/ts/ts-object-keys/EXPECTED.yaml +6 -0
- package/payload/extractor-fixtures/ts/ts-object-keys/sample.tsx +23 -0
- package/payload/extractor-fixtures/ts/ts-union/EXPECTED.yaml +5 -0
- package/payload/extractor-fixtures/ts/ts-union/sample.ts +9 -0
- package/payload/hooks/pre-commit +37 -0
- package/payload/hooks/reverse-lookup +66 -0
- package/payload/package.json +3 -0
- package/payload/protocol/.gitkeep +0 -0
- package/payload/protocol/AGENTS.md +239 -0
- package/payload/protocol/derived-layer.md +174 -0
- package/payload/protocol/new-kind-pipeline.md +179 -0
- package/payload/protocol/registry-warrant.md +162 -0
- package/payload/protocol/skills/kb-build.md +303 -0
- package/payload/protocol/skills/knowledge-audit.md +183 -0
- package/payload/protocol/skills/knowledge-bootstrap.md +229 -0
- package/payload/protocol/skills/knowledge-reflect.md +397 -0
- package/payload/schemas/catalog.schema.json +32 -0
- package/payload/schemas/decision-entry.schema.json +122 -0
- package/payload/schemas/finding.schema.json +77 -0
- package/payload/schemas/gap.schema.json +52 -0
- package/payload/schemas/graduation-categories.schema.json +64 -0
- package/payload/schemas/knowledge-leaf.schema.json +194 -0
- package/payload/schemas/miss.schema.json +45 -0
- package/payload/schemas/ontology-concept.schema.json +115 -0
- package/payload/schemas/phoenix-event.schema.json +76 -0
- package/payload/schemas/registry.schema.json +57 -0
- package/payload/schemas/rules.schema.json +14 -0
- package/payload/schemas/survey-scope.schema.json +23 -0
- package/payload/templates/decisions/_catalog.yaml +7 -0
- package/payload/templates/decisions/_registries/graduation-categories.yaml +42 -0
- package/payload/templates/decisions/phoenix-event.yaml +74 -0
- package/payload/templates/decisions/reflect-mint-proposal.yaml +100 -0
- package/payload/templates/decisions/registry-minting.yaml +58 -0
- package/payload/templates/decisions/trust-graduation.yaml +120 -0
- package/payload/templates/decisions/trust-revocation.yaml +106 -0
- package/payload/templates/knowledge/_catalog.yaml +9 -0
- package/payload/templates/knowledge/_registries/anchor.yaml +42 -0
- package/payload/templates/knowledge/_registries/authority-tiers.yaml +32 -0
- package/payload/templates/knowledge/_registries/domains.yaml +43 -0
- package/payload/templates/knowledge/_registries/form.yaml +38 -0
- package/payload/templates/knowledge/_registries/jurisdictions.yaml +20 -0
- package/payload/templates/knowledge/_registries/operations.yaml +18 -0
- package/payload/templates/knowledge/_registries/stage.yaml +53 -0
- package/payload/templates/knowledge/_rules.yaml +6 -0
- package/payload/templates/new-kind/README.md +107 -0
- package/payload/templates/new-kind/descriptor.example.yaml +18 -0
- package/payload/templates/new-kind/fixture/EXPECTED.yaml +6 -0
- package/payload/templates/new-kind/fixture/demo-anchor.list +2 -0
- package/payload/templates/new-kind/fixture/sample.list +7 -0
- package/payload/templates/new-kind/parser.example.js +98 -0
- package/payload/templates/ontology/_catalog.yaml +6 -0
- package/payload/templates/ontology/_rules.yaml +6 -0
- package/payload/wrappers/cursor.mdc +15 -0
- package/payload/wrappers/pointer.md +10 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema validation for store records (KK-02) — the layer the store loader
|
|
3
|
+
* (KK-04) calls before indexing anything.
|
|
4
|
+
*
|
|
5
|
+
* The formal contract lives in ../../schemas/*.schema.json (JSON Schema
|
|
6
|
+
* draft 2020-12, consumable by any external tool). This module interprets
|
|
7
|
+
* those documents directly — no schema library (D-002: js-yaml and little
|
|
8
|
+
* else) — supporting exactly the subset the schemas use: type, required,
|
|
9
|
+
* properties, additionalProperties:false, items, enum, pattern, minItems,
|
|
10
|
+
* minimum, and $ref into #/$defs.
|
|
11
|
+
*
|
|
12
|
+
* On top of the schemas it enforces the PRD §3.5 conventions JSON Schema
|
|
13
|
+
* cannot express record-locally:
|
|
14
|
+
* - enumerates values are strings, byte-exact, case-sensitive, as sets;
|
|
15
|
+
* non-string scalars hard-error (`non-string-enumerates-value`) — the
|
|
16
|
+
* YAML 1.1 coercion trap. Callers must parse YAML with types intact
|
|
17
|
+
* (js-yaml default schema) and pass the parsed document unchanged.
|
|
18
|
+
* - multi-entry source-of-truth: the first entry is the primary owner,
|
|
19
|
+
* additional entries are secondary references; every enumerates
|
|
20
|
+
* descriptor's source must name a listed entry
|
|
21
|
+
* (`enumerates-source-not-listed`).
|
|
22
|
+
* - every store file carries an integer schema-version >= 1; value defects
|
|
23
|
+
* there are normalized to `invalid-schema-version` (a stray record-level
|
|
24
|
+
* key stays `unknown-property` — the fix is to remove it, not retype it).
|
|
25
|
+
* Evolution is additive-only (D-013 generalized).
|
|
26
|
+
*
|
|
27
|
+
* Diagnostics carry a closed set of typed codes (PRD §5: protocol conduct
|
|
28
|
+
* keys on codes, not prose) and are stable-sorted by path then code so
|
|
29
|
+
* run-over-run diffs mean something.
|
|
30
|
+
*/
|
|
31
|
+
import { readFileSync } from 'node:fs';
|
|
32
|
+
import { ID_GRAMMARS, SCHEMA_DEFS } from './id-grammars.js';
|
|
33
|
+
|
|
34
|
+
/** Record kind → schema document shipped in payload/schemas/. */
|
|
35
|
+
const KIND_SCHEMA_FILES = Object.freeze({
|
|
36
|
+
'ontology-concept': 'ontology-concept.schema.json',
|
|
37
|
+
'knowledge-leaf': 'knowledge-leaf.schema.json',
|
|
38
|
+
'decision-entry': 'decision-entry.schema.json',
|
|
39
|
+
'finding': 'finding.schema.json',
|
|
40
|
+
'miss': 'miss.schema.json',
|
|
41
|
+
'gap': 'gap.schema.json',
|
|
42
|
+
'survey-scope': 'survey-scope.schema.json',
|
|
43
|
+
'catalog': 'catalog.schema.json',
|
|
44
|
+
'rules': 'rules.schema.json',
|
|
45
|
+
'registry': 'registry.schema.json',
|
|
46
|
+
'phoenix-event': 'phoenix-event.schema.json',
|
|
47
|
+
'graduation-categories': 'graduation-categories.schema.json',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const KINDS = Object.freeze(Object.keys(KIND_SCHEMA_FILES));
|
|
51
|
+
|
|
52
|
+
export const ERROR_CODES = Object.freeze([
|
|
53
|
+
'wrong-type',
|
|
54
|
+
'missing-required',
|
|
55
|
+
'unknown-property',
|
|
56
|
+
'invalid-enum-value',
|
|
57
|
+
'pattern-mismatch',
|
|
58
|
+
'too-few-items',
|
|
59
|
+
'out-of-range',
|
|
60
|
+
'invalid-schema-version',
|
|
61
|
+
'non-string-enumerates-value',
|
|
62
|
+
'duplicate-enumerates-value',
|
|
63
|
+
'enumerates-source-not-listed',
|
|
64
|
+
'lifecycle-field-mismatch',
|
|
65
|
+
'locator-shape',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The JSON Schema subset this module interprets. Schemas must not use
|
|
70
|
+
* keywords outside this set — an unenforced keyword is silent contract
|
|
71
|
+
* drift (tests/store-schemas.test.js walks every schema against it).
|
|
72
|
+
*/
|
|
73
|
+
export const SUPPORTED_KEYWORDS = Object.freeze([
|
|
74
|
+
'$schema', '$id', '$defs', '$ref',
|
|
75
|
+
'title', 'description',
|
|
76
|
+
'type', 'required', 'properties', 'additionalProperties',
|
|
77
|
+
'items', 'enum', 'pattern', 'minItems', 'minimum',
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Lexicographic comparator shared by every engine surface that emits
|
|
82
|
+
* stable-sorted, diffable output (PRD §5) — one ordering, one copy.
|
|
83
|
+
*/
|
|
84
|
+
export const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
85
|
+
|
|
86
|
+
const schemaCache = new Map();
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Bind the shared id grammars onto a freshly parsed schema (UCS-1142).
|
|
90
|
+
*
|
|
91
|
+
* The schema files keep their own `$defs` copies so each stays self-contained
|
|
92
|
+
* for external consumers, but the ENGINE validates against lib/id-grammars.js
|
|
93
|
+
* — the one module that owns each id space's pattern. Overwriting rather than
|
|
94
|
+
* asserting is deliberate: a drifted copy is corrected here, so the engine can
|
|
95
|
+
* only ever enforce the live grammar. (tests/id-grammars.test.js pins that the
|
|
96
|
+
* shipped copies agree, which is what keeps the published documents honest.)
|
|
97
|
+
*
|
|
98
|
+
* The grammar's HINT is bound alongside its pattern, under a key the schema
|
|
99
|
+
* keyword set does not interpret, so a pattern-mismatch on an id can say what
|
|
100
|
+
* shape was expected instead of quoting a regex at an author. That matters most
|
|
101
|
+
* where the shape CHANGED: since UCS-1147 a leaf citation must be an accession,
|
|
102
|
+
* and `"700.2" does not match ^L-[0-9]{6}$` tells an author what failed while
|
|
103
|
+
* naming neither the remedy nor the migration that moved it. The hint travels
|
|
104
|
+
* from the same frozen entry as the pattern, for the reason id-grammars.js
|
|
105
|
+
* exists — a hint that drifted from its pattern would describe the wrong shape.
|
|
106
|
+
*/
|
|
107
|
+
function bindIdGrammars(schema) {
|
|
108
|
+
for (const [space, def] of Object.entries(SCHEMA_DEFS)) {
|
|
109
|
+
const node = schema.$defs?.[def];
|
|
110
|
+
if (node) {
|
|
111
|
+
node.pattern = ID_GRAMMARS[space].pattern;
|
|
112
|
+
node.hint = ID_GRAMMARS[space].hint;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return schema;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function schemaFor(kind) {
|
|
119
|
+
const file = KIND_SCHEMA_FILES[kind];
|
|
120
|
+
if (!file) {
|
|
121
|
+
throw new TypeError(`unknown record kind "${kind}" (expected one of: ${KINDS.join(', ')})`);
|
|
122
|
+
}
|
|
123
|
+
if (!schemaCache.has(kind)) {
|
|
124
|
+
const url = new URL(`../../schemas/${file}`, import.meta.url);
|
|
125
|
+
schemaCache.set(kind, bindIdGrammars(JSON.parse(readFileSync(url, 'utf8'))));
|
|
126
|
+
}
|
|
127
|
+
return schemaCache.get(kind);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isPlainObject(value) {
|
|
131
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function describe(value) {
|
|
135
|
+
if (value === null) return 'null';
|
|
136
|
+
if (Array.isArray(value)) return 'array';
|
|
137
|
+
if (value instanceof Date) return 'date (quote it)';
|
|
138
|
+
return typeof value;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function typeMatches(type, value) {
|
|
142
|
+
if (Array.isArray(type)) return type.some((t) => typeMatches(t, value));
|
|
143
|
+
switch (type) {
|
|
144
|
+
case 'string': return typeof value === 'string';
|
|
145
|
+
case 'integer': return Number.isInteger(value);
|
|
146
|
+
case 'number': return typeof value === 'number' && Number.isFinite(value);
|
|
147
|
+
case 'boolean': return typeof value === 'boolean';
|
|
148
|
+
case 'array': return Array.isArray(value);
|
|
149
|
+
case 'object': return isPlainObject(value);
|
|
150
|
+
case 'null': return value === null;
|
|
151
|
+
default: throw new Error(`schema uses unsupported type "${type}"`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function joinPath(base, key) {
|
|
156
|
+
return base ? `${base}.${key}` : key;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const regexCache = new Map();
|
|
160
|
+
|
|
161
|
+
function regexFor(pattern) {
|
|
162
|
+
let re = regexCache.get(pattern);
|
|
163
|
+
if (!re) {
|
|
164
|
+
re = new RegExp(pattern);
|
|
165
|
+
regexCache.set(pattern, re);
|
|
166
|
+
}
|
|
167
|
+
return re;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const refCache = new WeakMap();
|
|
171
|
+
|
|
172
|
+
function resolveRef(root, ref) {
|
|
173
|
+
if (ref === '#') return root;
|
|
174
|
+
let refs = refCache.get(root);
|
|
175
|
+
if (!refs) {
|
|
176
|
+
refs = new Map();
|
|
177
|
+
refCache.set(root, refs);
|
|
178
|
+
}
|
|
179
|
+
let target = refs.get(ref);
|
|
180
|
+
if (target === undefined) {
|
|
181
|
+
const match = /^#\/\$defs\/([^/]+)$/.exec(ref);
|
|
182
|
+
target = (match && root.$defs?.[match[1]]) ?? null;
|
|
183
|
+
refs.set(ref, target);
|
|
184
|
+
}
|
|
185
|
+
if (!target) throw new Error(`schema $ref "${ref}" does not resolve`);
|
|
186
|
+
return target;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The id-grammar hint a required property carries, if it declares one.
|
|
191
|
+
*
|
|
192
|
+
* Only reaches a hint bound by bindIdGrammars — a property whose shape is an id
|
|
193
|
+
* space. Everything else returns null and the plain message stands, which is
|
|
194
|
+
* the right default: "required property "heading" is missing" needs no gloss,
|
|
195
|
+
* and a hint invented per field would be prose with nothing keeping it true.
|
|
196
|
+
*
|
|
197
|
+
* A `$ref` that does not resolve is swallowed rather than thrown: this runs
|
|
198
|
+
* only to DECORATE a defect the validator has already found, and a schema
|
|
199
|
+
* authoring error must not turn a legitimate finding into a crash.
|
|
200
|
+
*
|
|
201
|
+
* @param {object} root the schema document, for $defs resolution
|
|
202
|
+
* @param {object} schema the object schema declaring the required property
|
|
203
|
+
* @param {string} required the missing property's name
|
|
204
|
+
* @returns {string|null} the hint, or null when the property declares none
|
|
205
|
+
*/
|
|
206
|
+
function requiredHint(root, schema, required) {
|
|
207
|
+
const property = schema.properties?.[required];
|
|
208
|
+
if (!property) return null;
|
|
209
|
+
try {
|
|
210
|
+
const target = property.$ref ? resolveRef(root, property.$ref) : property;
|
|
211
|
+
return typeof target.hint === 'string' ? target.hint : null;
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Interpret the supported JSON Schema subset against a value. */
|
|
218
|
+
function check(root, schema, value, path, errors) {
|
|
219
|
+
if (schema.$ref) {
|
|
220
|
+
check(root, resolveRef(root, schema.$ref), value, path, errors);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (schema.type && !typeMatches(schema.type, value)) {
|
|
224
|
+
errors.push({
|
|
225
|
+
path,
|
|
226
|
+
code: 'wrong-type',
|
|
227
|
+
message: `expected ${schema.type}, got ${describe(value)}`,
|
|
228
|
+
});
|
|
229
|
+
return; // deeper checks are meaningless on the wrong shape
|
|
230
|
+
}
|
|
231
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
232
|
+
errors.push({
|
|
233
|
+
path,
|
|
234
|
+
code: 'invalid-enum-value',
|
|
235
|
+
message: `${JSON.stringify(value)} is not one of: ${schema.enum.join(' | ')}`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (schema.pattern && typeof value === 'string' && !regexFor(schema.pattern).test(value)) {
|
|
239
|
+
errors.push({
|
|
240
|
+
path,
|
|
241
|
+
code: 'pattern-mismatch',
|
|
242
|
+
// An id-space pattern arrives with the hint bound by bindIdGrammars, and
|
|
243
|
+
// the hint REPLACES the regex rather than trailing it: it says everything
|
|
244
|
+
// the pattern says and says it to a human, so printing both would just
|
|
245
|
+
// make the author read the regex first. Patterns from the schema files
|
|
246
|
+
// themselves carry no hint and keep quoting the regex — there is nothing
|
|
247
|
+
// better to say about them, and inventing prose per pattern here would be
|
|
248
|
+
// the drift id-grammars.js exists to prevent.
|
|
249
|
+
message: schema.hint
|
|
250
|
+
? `${JSON.stringify(value)} is not a valid id here — expected ${schema.hint}`
|
|
251
|
+
: `${JSON.stringify(value)} does not match ${schema.pattern}`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (schema.minimum !== undefined && typeof value === 'number' && value < schema.minimum) {
|
|
255
|
+
errors.push({
|
|
256
|
+
path,
|
|
257
|
+
code: 'out-of-range',
|
|
258
|
+
message: `${value} is below the minimum of ${schema.minimum}`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (Array.isArray(value)) {
|
|
262
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
263
|
+
errors.push({
|
|
264
|
+
path,
|
|
265
|
+
code: 'too-few-items',
|
|
266
|
+
message: `expected at least ${schema.minItems} item(s), got ${value.length}`,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
if (schema.items) {
|
|
270
|
+
value.forEach((item, i) => check(root, schema.items, item, `${path}[${i}]`, errors));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (isPlainObject(value)) {
|
|
274
|
+
for (const required of schema.required ?? []) {
|
|
275
|
+
if (!Object.hasOwn(value, required)) {
|
|
276
|
+
// A missing property has no value to test, so the pattern branch above
|
|
277
|
+
// can never speak for it — yet "required property "id" is missing" is
|
|
278
|
+
// exactly the finding an author gets for a leaf that mints no accession,
|
|
279
|
+
// and on its own it names neither the shape to write nor why the field
|
|
280
|
+
// became required. So the hint is read from the property's OWN grammar
|
|
281
|
+
// here, through the same `$defs` binding: one fact, quoted wherever the
|
|
282
|
+
// author meets it.
|
|
283
|
+
const hint = requiredHint(root, schema, required);
|
|
284
|
+
errors.push({
|
|
285
|
+
path: joinPath(path, required),
|
|
286
|
+
code: 'missing-required',
|
|
287
|
+
message: hint
|
|
288
|
+
? `required property "${required}" is missing — expected ${hint}`
|
|
289
|
+
: `required property "${required}" is missing`,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const properties = schema.properties ?? {};
|
|
294
|
+
for (const [key, propertyValue] of Object.entries(value)) {
|
|
295
|
+
if (Object.hasOwn(properties, key)) {
|
|
296
|
+
check(root, properties[key], propertyValue, joinPath(path, key), errors);
|
|
297
|
+
} else if (schema.additionalProperties === false) {
|
|
298
|
+
errors.push({
|
|
299
|
+
path: joinPath(path, key),
|
|
300
|
+
code: 'unknown-property',
|
|
301
|
+
message: `unknown property "${key}" (additive schema evolution edits the schema; unknown keys are typos)`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* PRD §3.5 conventions on one ontology concept — the checks JSON Schema
|
|
310
|
+
* cannot express record-locally.
|
|
311
|
+
*/
|
|
312
|
+
function conceptConventions(record, basePath, errors) {
|
|
313
|
+
if (!isPlainObject(record) || !Array.isArray(record.enumerates)) return;
|
|
314
|
+
const sourceOfTruth = new Set(Array.isArray(record['source-of-truth'])
|
|
315
|
+
? record['source-of-truth'].filter((entry) => typeof entry === 'string')
|
|
316
|
+
: []);
|
|
317
|
+
record.enumerates.forEach((descriptor, i) => {
|
|
318
|
+
if (!isPlainObject(descriptor)) return;
|
|
319
|
+
const descriptorPath = joinPath(basePath, `enumerates[${i}]`);
|
|
320
|
+
if (Array.isArray(descriptor.values)) {
|
|
321
|
+
const seen = new Set();
|
|
322
|
+
descriptor.values.forEach((value, j) => {
|
|
323
|
+
if (typeof value !== 'string') {
|
|
324
|
+
errors.push({
|
|
325
|
+
path: `${descriptorPath}.values[${j}]`,
|
|
326
|
+
code: 'non-string-enumerates-value',
|
|
327
|
+
message: `enumerates values are strings compared byte-exact and case-sensitive (§3.5); got ${describe(value)} (${JSON.stringify(value)}) — YAML coerces bare scalars like true/1.0/null, so quote the value in the store file`,
|
|
328
|
+
});
|
|
329
|
+
} else if (seen.has(value)) {
|
|
330
|
+
errors.push({
|
|
331
|
+
path: `${descriptorPath}.values[${j}]`,
|
|
332
|
+
code: 'duplicate-enumerates-value',
|
|
333
|
+
message: `duplicate enumerates value ${JSON.stringify(value)} — values are compared as sets (§3.5), so a duplicate is a malformed descriptor, never a bigger set`,
|
|
334
|
+
});
|
|
335
|
+
} else {
|
|
336
|
+
seen.add(value);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
if (typeof descriptor.source === 'string' && !sourceOfTruth.has(descriptor.source)) {
|
|
341
|
+
errors.push({
|
|
342
|
+
path: `${descriptorPath}.source`,
|
|
343
|
+
code: 'enumerates-source-not-listed',
|
|
344
|
+
message: `enumerates source ${JSON.stringify(descriptor.source)} must name a listed source-of-truth entry (§3.5: first entry is the primary owner, the rest are secondary references)`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* §3.4 lifecycle invariants on one log fragment (finding/miss/gap) — the
|
|
352
|
+
* checks JSON Schema cannot express record-locally. The schema gate matches
|
|
353
|
+
* what the transition helper (log-entry.js) enforces on its write path, so a
|
|
354
|
+
* hand-edited fragment can't carry an inconsistent lifecycle:
|
|
355
|
+
* - verified ⇔ status resolved (both directions)
|
|
356
|
+
* - rejected ⇒ non-empty reason; reason travels only with rejected
|
|
357
|
+
*/
|
|
358
|
+
function lifecycleConventions(record, basePath, errors) {
|
|
359
|
+
if (!isPlainObject(record) || typeof record.status !== 'string') return;
|
|
360
|
+
const { status } = record;
|
|
361
|
+
if (Object.hasOwn(record, 'verified') !== (status === 'resolved')) {
|
|
362
|
+
errors.push({
|
|
363
|
+
path: joinPath(basePath, 'verified'),
|
|
364
|
+
code: 'lifecycle-field-mismatch',
|
|
365
|
+
message: status === 'resolved'
|
|
366
|
+
? 'a resolved entry carries the verified date the validator re-run stamped (§8)'
|
|
367
|
+
: `verified travels only with status "resolved", not ${JSON.stringify(status)} — re-opening drops it (§8)`,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
const hasReason = typeof record.reason === 'string' && record.reason !== '';
|
|
371
|
+
if (status === 'rejected' ? !hasReason : Object.hasOwn(record, 'reason')) {
|
|
372
|
+
errors.push({
|
|
373
|
+
path: joinPath(basePath, 'reason'),
|
|
374
|
+
code: 'lifecycle-field-mismatch',
|
|
375
|
+
message: status === 'rejected'
|
|
376
|
+
? 'rejecting requires a non-empty reason — rejections record the reason (§8)'
|
|
377
|
+
: `reason travels only with status "rejected", not ${JSON.stringify(status)} — re-opening drops it (§8)`,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* A document candidate's `section` locator addresses ONE coordinate system
|
|
384
|
+
* (UCS-1160). `lib/coverage.js` emits `{line, endLine}` for line-addressed
|
|
385
|
+
* sources and `{page, object}` for page-addressed ones (pdf) — never both,
|
|
386
|
+
* because a section does not have a line number AND a page number in the same
|
|
387
|
+
* document. So the finding fragment must carry exactly one:
|
|
388
|
+
* - NEITHER is an underspecified locator — "somewhere in this document" is
|
|
389
|
+
* the coordinate-free claim the locator exists to replace, and reflect
|
|
390
|
+
* would have to re-read the document it was meant to avoid opening.
|
|
391
|
+
* - BOTH is contradictory — two coordinate systems disagreeing about where
|
|
392
|
+
* the section is, with nothing to say which one a reader should trust.
|
|
393
|
+
*
|
|
394
|
+
* This is a CONVENTION rather than a schema keyword because the JSON Schema
|
|
395
|
+
* subset this module interprets (SUPPORTED_KEYWORDS) has no conditionals —
|
|
396
|
+
* no `oneOf`, `anyOf`, `allOf`, or `not`. Writing one into the schema would
|
|
397
|
+
* add a keyword nothing enforces, which is silent contract drift: the schema
|
|
398
|
+
* would promise a rule no validator checks. The rule lives here, where finding
|
|
399
|
+
* records are already judged, and the schema's description says so.
|
|
400
|
+
*/
|
|
401
|
+
function locatorConventions(record, basePath, errors) {
|
|
402
|
+
if (!isPlainObject(record) || !isPlainObject(record.section)) return;
|
|
403
|
+
const { section } = record;
|
|
404
|
+
const hasLine = Object.hasOwn(section, 'line');
|
|
405
|
+
const hasPage = Object.hasOwn(section, 'page');
|
|
406
|
+
if (hasLine === hasPage) {
|
|
407
|
+
errors.push({
|
|
408
|
+
path: joinPath(basePath, 'section'),
|
|
409
|
+
code: 'locator-shape',
|
|
410
|
+
message: hasLine
|
|
411
|
+
? 'a section locator addresses one coordinate system: line (line-addressed sources) or page (pdf), never both — coverage emits exactly one (UCS-1160)'
|
|
412
|
+
: 'a section locator needs a coordinate: line for line-addressed sources, page for pdf — a locator with neither cannot open the section it addresses (UCS-1160)',
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Findings carry the lifecycle every log fragment carries, plus the locator
|
|
419
|
+
* shape only they can have (a miss/gap has no `section`).
|
|
420
|
+
*/
|
|
421
|
+
function findingConventions(record, basePath, errors) {
|
|
422
|
+
lifecycleConventions(record, basePath, errors);
|
|
423
|
+
locatorConventions(record, basePath, errors);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Record-local convention checks, keyed by kind (one lookup, both entry points). */
|
|
427
|
+
const CONVENTIONS = Object.freeze({
|
|
428
|
+
'ontology-concept': conceptConventions,
|
|
429
|
+
finding: findingConventions,
|
|
430
|
+
miss: lifecycleConventions,
|
|
431
|
+
gap: lifecycleConventions,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* schema-version defects where the envelope legitimately carries the key are
|
|
436
|
+
* normalized to `invalid-schema-version`. Only value defects qualify — an
|
|
437
|
+
* `unknown-property` at that path means the key is misplaced (it belongs on
|
|
438
|
+
* the store-file envelope), which is the opposite remediation.
|
|
439
|
+
*/
|
|
440
|
+
const SCHEMA_VERSION_VALUE_DEFECTS = new Set(['wrong-type', 'missing-required', 'out-of-range']);
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Dedupe (convention codes own their paths over generic schema codes),
|
|
444
|
+
* normalize schema-version value defects to one code, and stable-sort.
|
|
445
|
+
*/
|
|
446
|
+
function finish(errors, { fileEnvelope }) {
|
|
447
|
+
const conventionPaths = new Set(
|
|
448
|
+
errors
|
|
449
|
+
.filter((e) => e.code === 'non-string-enumerates-value')
|
|
450
|
+
.map((e) => e.path),
|
|
451
|
+
);
|
|
452
|
+
const cleaned = errors
|
|
453
|
+
.filter((e) => e.code === 'non-string-enumerates-value' || !conventionPaths.has(e.path))
|
|
454
|
+
.map((e) => (fileEnvelope && e.path === 'schema-version' && SCHEMA_VERSION_VALUE_DEFECTS.has(e.code)
|
|
455
|
+
? { ...e, code: 'invalid-schema-version', message: `${e.message} — every store file carries an integer schema-version >= 1 (§3.5)` }
|
|
456
|
+
: e));
|
|
457
|
+
cleaned.sort((a, b) => compare(a.path, b.path) || compare(a.code, b.code));
|
|
458
|
+
return { ok: cleaned.length === 0, errors: cleaned };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function runConventions(kind, record, basePath, errors) {
|
|
462
|
+
CONVENTIONS[kind]?.(record, basePath, errors);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Validate a single record of the given kind against its shipped schema.
|
|
467
|
+
* For one-record-per-file kinds (knowledge-leaf, finding, catalog, rules)
|
|
468
|
+
* the record is the whole store file, schema-version included.
|
|
469
|
+
*
|
|
470
|
+
* @param {string} kind one of KINDS
|
|
471
|
+
* @param {unknown} record the YAML document (or entry), parsed with types intact
|
|
472
|
+
* @returns {{ ok: boolean, errors: Array<{path: string, code: string, message: string}> }}
|
|
473
|
+
*/
|
|
474
|
+
export function validateRecord(kind, record) {
|
|
475
|
+
const schema = schemaFor(kind);
|
|
476
|
+
const errors = [];
|
|
477
|
+
check(schema, schema, record, '', errors);
|
|
478
|
+
runConventions(kind, record, '', errors);
|
|
479
|
+
// One-record-per-file kinds ARE the store file (schema-version at the root);
|
|
480
|
+
// multi-entry kinds carry it on the envelope, not the record.
|
|
481
|
+
return finish(errors, { fileEnvelope: !schema.$defs?.storeFile });
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Validate a whole store file of the given kind. Multi-entry kinds
|
|
486
|
+
* (ontology-concept, decision-entry) are wrapped in the $defs/storeFile
|
|
487
|
+
* envelope ({ schema-version, entries: [...] }); one-record-per-file kinds
|
|
488
|
+
* validate as the record itself.
|
|
489
|
+
*
|
|
490
|
+
* @param {string} kind one of KINDS
|
|
491
|
+
* @param {unknown} doc the parsed store file
|
|
492
|
+
* @returns {{ ok: boolean, errors: Array<{path: string, code: string, message: string}> }}
|
|
493
|
+
*/
|
|
494
|
+
export function validateStoreFile(kind, doc) {
|
|
495
|
+
const schema = schemaFor(kind);
|
|
496
|
+
const fileSchema = schema.$defs?.storeFile;
|
|
497
|
+
if (!fileSchema) return validateRecord(kind, doc);
|
|
498
|
+
const errors = [];
|
|
499
|
+
check(schema, fileSchema, doc, '', errors);
|
|
500
|
+
if (isPlainObject(doc) && Array.isArray(doc.entries)) {
|
|
501
|
+
doc.entries.forEach((entry, i) => runConventions(kind, entry, `entries[${i}]`, errors));
|
|
502
|
+
}
|
|
503
|
+
return finish(errors, { fileEnvelope: true });
|
|
504
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* engine/log-entry.js — the entry shim (UCS-956).
|
|
4
|
+
*
|
|
5
|
+
* This file statically imports NOTHING. That is its entire job.
|
|
6
|
+
*
|
|
7
|
+
* Node exits 1 on an unhandled ES module load error, and exit 1 means FINDINGS
|
|
8
|
+
* (PRD §5). A SyntaxError in a lib/ module, or a missing `js-yaml`, must not
|
|
9
|
+
* tell an agent that the check ran and found problems. So the engine is reached
|
|
10
|
+
* only through `import()`, where a load failure is an ordinary catchable
|
|
11
|
+
* rejection rather than a process-level crash.
|
|
12
|
+
*
|
|
13
|
+
* Both specifiers are string literals naming the engine's own files. D-014
|
|
14
|
+
* forbids importing REPO CONTENT — the client's code — and nothing here can
|
|
15
|
+
* name it: there is no variable to point somewhere else.
|
|
16
|
+
*
|
|
17
|
+
* The command lives in commands/log-entry.js. Its invocation path is unchanged.
|
|
18
|
+
*/
|
|
19
|
+
try {
|
|
20
|
+
const [{ boot }, command] = await Promise.all([
|
|
21
|
+
import('./lib/boot.js'),
|
|
22
|
+
import('./commands/log-entry.js'),
|
|
23
|
+
]);
|
|
24
|
+
// exitCode, never process.exit(): exit() drops queued async stdout writes, so
|
|
25
|
+
// piped --json output would truncate at the pipe buffer — corrupt output
|
|
26
|
+
// wearing a clean exit code. Node exits on its own once stdout drains.
|
|
27
|
+
process.exitCode = await boot('log-entry', command);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
// The engine could not be loaded, so no check ran. Exit 2 — never 1.
|
|
30
|
+
// Hardcoded, because reading it from lib/exit-codes.js is the very thing
|
|
31
|
+
// that may have just failed.
|
|
32
|
+
process.stderr.write(`log-entry: internal failure — the engine could not be loaded\n${error?.stack ?? error}\n`);
|
|
33
|
+
process.exitCode = 2;
|
|
34
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* engine/phoenix.js — the entry shim (UCS-956).
|
|
4
|
+
*
|
|
5
|
+
* This file statically imports NOTHING. That is its entire job.
|
|
6
|
+
*
|
|
7
|
+
* Node exits 1 on an unhandled ES module load error, and exit 1 means FINDINGS
|
|
8
|
+
* (PRD §5). A SyntaxError in a lib/ module, or a missing `js-yaml`, must not
|
|
9
|
+
* tell an agent that the check ran and found problems. So the engine is reached
|
|
10
|
+
* only through `import()`, where a load failure is an ordinary catchable
|
|
11
|
+
* rejection rather than a process-level crash.
|
|
12
|
+
*
|
|
13
|
+
* The stakes are highest here: this is the one command that WRITES to a store
|
|
14
|
+
* (UCS-1154). A load failure that wore exit 1 would read as "the event was
|
|
15
|
+
* refused with findings" — which is the one outcome that also means "nothing
|
|
16
|
+
* was written", and would be indistinguishable from an event that half-applied.
|
|
17
|
+
*
|
|
18
|
+
* Both specifiers are string literals naming the engine's own files. D-014
|
|
19
|
+
* forbids importing REPO CONTENT — the client's code — and nothing here can
|
|
20
|
+
* name it: there is no variable to point somewhere else.
|
|
21
|
+
*
|
|
22
|
+
* The command lives in commands/phoenix.js. Its invocation path is unchanged.
|
|
23
|
+
*/
|
|
24
|
+
try {
|
|
25
|
+
const [{ boot }, command] = await Promise.all([
|
|
26
|
+
import('./lib/boot.js'),
|
|
27
|
+
import('./commands/phoenix.js'),
|
|
28
|
+
]);
|
|
29
|
+
// exitCode, never process.exit(): exit() drops queued async stdout writes, so
|
|
30
|
+
// piped --json output would truncate at the pipe buffer — corrupt output
|
|
31
|
+
// wearing a clean exit code. Node exits on its own once stdout drains.
|
|
32
|
+
process.exitCode = await boot('phoenix', command);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// The engine could not be loaded, so no event ran. Exit 2 — never 1.
|
|
35
|
+
// Hardcoded, because reading it from lib/exit-codes.js is the very thing
|
|
36
|
+
// that may have just failed.
|
|
37
|
+
process.stderr.write(`phoenix: internal failure — the engine could not be loaded\n${error?.stack ?? error}\n`);
|
|
38
|
+
process.exitCode = 2;
|
|
39
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* engine/preflight.js — the entry shim (UCS-956).
|
|
4
|
+
*
|
|
5
|
+
* This file statically imports NOTHING. That is its entire job.
|
|
6
|
+
*
|
|
7
|
+
* Node exits 1 on an unhandled ES module load error, and exit 1 means FINDINGS
|
|
8
|
+
* (PRD §5). A SyntaxError in a lib/ module, or a missing `js-yaml`, must not
|
|
9
|
+
* tell an agent that the check ran and found problems. So the engine is reached
|
|
10
|
+
* only through `import()`, where a load failure is an ordinary catchable
|
|
11
|
+
* rejection rather than a process-level crash.
|
|
12
|
+
*
|
|
13
|
+
* Both specifiers are string literals naming the engine's own files. D-014
|
|
14
|
+
* forbids importing REPO CONTENT — the client's code — and nothing here can
|
|
15
|
+
* name it: there is no variable to point somewhere else.
|
|
16
|
+
*
|
|
17
|
+
* The command lives in commands/preflight.js. Its invocation path is unchanged.
|
|
18
|
+
*/
|
|
19
|
+
try {
|
|
20
|
+
const [{ boot }, command] = await Promise.all([
|
|
21
|
+
import('./lib/boot.js'),
|
|
22
|
+
import('./commands/preflight.js'),
|
|
23
|
+
]);
|
|
24
|
+
// exitCode, never process.exit(): exit() drops queued async stdout writes, so
|
|
25
|
+
// piped --json output would truncate at the pipe buffer — corrupt output
|
|
26
|
+
// wearing a clean exit code. Node exits on its own once stdout drains.
|
|
27
|
+
process.exitCode = await boot('preflight', command);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
// The engine could not be loaded, so no check ran. Exit 2 — never 1.
|
|
30
|
+
// Hardcoded, because reading it from lib/exit-codes.js is the very thing
|
|
31
|
+
// that may have just failed.
|
|
32
|
+
process.stderr.write(`preflight: internal failure — the engine could not be loaded\n${error?.stack ?? error}\n`);
|
|
33
|
+
process.exitCode = 2;
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* engine/resolve.js — the entry shim (UCS-956).
|
|
4
|
+
*
|
|
5
|
+
* This file statically imports NOTHING. That is its entire job.
|
|
6
|
+
*
|
|
7
|
+
* Node exits 1 on an unhandled ES module load error, and exit 1 means FINDINGS
|
|
8
|
+
* (PRD §5). A SyntaxError in a lib/ module, or a missing `js-yaml`, must not
|
|
9
|
+
* tell an agent that the check ran and found problems. So the engine is reached
|
|
10
|
+
* only through `import()`, where a load failure is an ordinary catchable
|
|
11
|
+
* rejection rather than a process-level crash.
|
|
12
|
+
*
|
|
13
|
+
* Both specifiers are string literals naming the engine's own files. D-014
|
|
14
|
+
* forbids importing REPO CONTENT — the client's code — and nothing here can
|
|
15
|
+
* name it: there is no variable to point somewhere else.
|
|
16
|
+
*
|
|
17
|
+
* The command lives in commands/resolve.js. Its invocation path is unchanged.
|
|
18
|
+
*/
|
|
19
|
+
try {
|
|
20
|
+
const [{ boot }, command] = await Promise.all([
|
|
21
|
+
import('./lib/boot.js'),
|
|
22
|
+
import('./commands/resolve.js'),
|
|
23
|
+
]);
|
|
24
|
+
// exitCode, never process.exit(): exit() drops queued async stdout writes, so
|
|
25
|
+
// piped --json output would truncate at the pipe buffer — corrupt output
|
|
26
|
+
// wearing a clean exit code. Node exits on its own once stdout drains.
|
|
27
|
+
process.exitCode = await boot('resolve', command);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
// The engine could not be loaded, so no check ran. Exit 2 — never 1.
|
|
30
|
+
// Hardcoded, because reading it from lib/exit-codes.js is the very thing
|
|
31
|
+
// that may have just failed.
|
|
32
|
+
process.stderr.write(`resolve: internal failure — the engine could not be loaded\n${error?.stack ?? error}\n`);
|
|
33
|
+
process.exitCode = 2;
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* engine/survey-map.js — the entry shim (UCS-956).
|
|
4
|
+
*
|
|
5
|
+
* This file statically imports NOTHING. That is its entire job.
|
|
6
|
+
*
|
|
7
|
+
* Node exits 1 on an unhandled ES module load error, and exit 1 means FINDINGS
|
|
8
|
+
* (PRD §5). A SyntaxError in a lib/ module, or a missing `js-yaml`, must not
|
|
9
|
+
* tell an agent that the check ran and found problems. So the engine is reached
|
|
10
|
+
* only through `import()`, where a load failure is an ordinary catchable
|
|
11
|
+
* rejection rather than a process-level crash.
|
|
12
|
+
*
|
|
13
|
+
* Both specifiers are string literals naming the engine's own files. D-014
|
|
14
|
+
* forbids importing REPO CONTENT — the client's code — and nothing here can
|
|
15
|
+
* name it: there is no variable to point somewhere else.
|
|
16
|
+
*
|
|
17
|
+
* The command lives in commands/survey-map.js. Its invocation path is unchanged.
|
|
18
|
+
*/
|
|
19
|
+
try {
|
|
20
|
+
const [{ boot }, command] = await Promise.all([
|
|
21
|
+
import('./lib/boot.js'),
|
|
22
|
+
import('./commands/survey-map.js'),
|
|
23
|
+
]);
|
|
24
|
+
// exitCode, never process.exit(): exit() drops queued async stdout writes, so
|
|
25
|
+
// piped --json output would truncate at the pipe buffer — corrupt output
|
|
26
|
+
// wearing a clean exit code. Node exits on its own once stdout drains.
|
|
27
|
+
process.exitCode = await boot('survey-map', command);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
// The engine could not be loaded, so no check ran. Exit 2 — never 1.
|
|
30
|
+
// Hardcoded, because reading it from lib/exit-codes.js is the very thing
|
|
31
|
+
// that may have just failed.
|
|
32
|
+
process.stderr.write(`survey-map: internal failure — the engine could not be loaded\n${error?.stack ?? error}\n`);
|
|
33
|
+
process.exitCode = 2;
|
|
34
|
+
}
|