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,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* engine/derive.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/derive.js. Its invocation path is unchanged.
|
|
18
|
+
*/
|
|
19
|
+
try {
|
|
20
|
+
const [{ boot }, command] = await Promise.all([
|
|
21
|
+
import('./lib/boot.js'),
|
|
22
|
+
import('./commands/derive.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('derive', 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(`derive: 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/ingest.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/ingest.js.
|
|
18
|
+
*/
|
|
19
|
+
try {
|
|
20
|
+
const [{ boot }, command] = await Promise.all([
|
|
21
|
+
import('./lib/boot.js'),
|
|
22
|
+
import('./commands/ingest.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('ingest', command);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
// The engine could not be loaded, so nothing was adapted. 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(`ingest: internal failure — the engine could not be loaded\n${error?.stack ?? error}\n`);
|
|
33
|
+
process.exitCode = 2;
|
|
34
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anchor-candidate signatures (KK-25) — ONE regex table shared between the
|
|
3
|
+
* survey map's pre-scan and the extractor kinds (KK-08/09 import from here).
|
|
4
|
+
* A kind describes the shape of a declaration, not the file type (PRD §5.1):
|
|
5
|
+
* .tsx/.jsx are extensions handled by the TS/JS kinds, not separate kinds.
|
|
6
|
+
*
|
|
7
|
+
* Each signature is a cheap lexical sniff — "this file plausibly contains an
|
|
8
|
+
* anchor of this kind" — never a parse. Extraction (and the syntactic-envelope
|
|
9
|
+
* hard-error discipline) belongs to the extractor recipes; the survey map only
|
|
10
|
+
* surfaces candidates for agent triage. Patterns are stored as source strings
|
|
11
|
+
* so the table stays data (importable, testable, greppable), compiled by
|
|
12
|
+
* consumers. `pattern: null` marks a directory-shape kind (dir-modules) that
|
|
13
|
+
* is detected structurally, not by content.
|
|
14
|
+
*
|
|
15
|
+
* Sorted by kind; a kind may carry several signatures when one anchor shape
|
|
16
|
+
* lives in more than one file format (strings-keys: legacy `.strings` and the
|
|
17
|
+
* JSON `.xcstrings` catalog). DEEP-frozen — the entries AND their extensions
|
|
18
|
+
* arrays — so extending the kind set stays a KK-08/09-style change with
|
|
19
|
+
* fixtures, never an in-session mutation (D-005).
|
|
20
|
+
*
|
|
21
|
+
* Known limitation: legacy UTF-16 .strings files (BOM-marked, common in older
|
|
22
|
+
* Xcode exports) do not decode as UTF-8, so a content sniff cannot see their
|
|
23
|
+
* `"key" = "value"` lines. The survey map treats a UTF-16 BOM as "candidate by
|
|
24
|
+
* extension" instead of skipping silently — see sniffKinds() in survey-map.js.
|
|
25
|
+
*/
|
|
26
|
+
const TS_EXTENSIONS = Object.freeze(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
|
27
|
+
const YAML_EXTENSIONS = Object.freeze(['.yaml', '.yml']);
|
|
28
|
+
|
|
29
|
+
export const ANCHOR_SIGNATURES = Object.freeze([
|
|
30
|
+
{
|
|
31
|
+
kind: 'dir-modules',
|
|
32
|
+
extensions: null, // directory shape: siblings sharing one extension
|
|
33
|
+
pattern: null,
|
|
34
|
+
flags: '',
|
|
35
|
+
reads: 'subfolder/file names, with pattern/strip options',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
kind: 'json-keys',
|
|
39
|
+
extensions: ['.json'],
|
|
40
|
+
pattern: '^\\s*"[^"\\n]+"\\s*:',
|
|
41
|
+
flags: 'm',
|
|
42
|
+
reads: 'top-level keys of a JSON object',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
kind: 'json-map-keys',
|
|
46
|
+
extensions: ['.json'],
|
|
47
|
+
pattern: '"[^"\\n]+"\\s*:\\s*\\{',
|
|
48
|
+
flags: 'm',
|
|
49
|
+
reads: 'keys under a dotted path in a JSON object',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
kind: 'strings-keys',
|
|
53
|
+
extensions: ['.strings'],
|
|
54
|
+
pattern: '^\\s*"[^"\\n]+"\\s*=\\s*"',
|
|
55
|
+
flags: 'm',
|
|
56
|
+
reads: 'localization keys of a legacy `"key" = "value"` .strings table',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
kind: 'strings-keys',
|
|
60
|
+
extensions: ['.xcstrings'],
|
|
61
|
+
pattern: '"strings"\\s*:\\s*\\{',
|
|
62
|
+
flags: 'm',
|
|
63
|
+
reads: 'localization keys of an .xcstrings JSON catalog ("strings" map)',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
kind: 'swift-const-array',
|
|
67
|
+
extensions: ['.swift'],
|
|
68
|
+
pattern: '\\b(?:static\\s+)?let\\s+\\w+\\s*(?::\\s*\\[[^\\]\\n]*\\])?\\s*=\\s*\\[',
|
|
69
|
+
flags: 'm',
|
|
70
|
+
reads: 'static let / let array literals',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
kind: 'swift-enum',
|
|
74
|
+
extensions: ['.swift'],
|
|
75
|
+
pattern: '\\benum\\s+\\w+\\s*(?::\\s*[^{\\n]+)?\\{',
|
|
76
|
+
flags: 'm',
|
|
77
|
+
reads: 'cases of a Swift enum',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
kind: 'ts-const-array',
|
|
81
|
+
extensions: TS_EXTENSIONS,
|
|
82
|
+
pattern: '\\bexport\\s+(?:const|let|var)\\s+\\w+\\s*(?::\\s*[^=\\n]+)?=\\s*\\[',
|
|
83
|
+
flags: 'm',
|
|
84
|
+
reads: 'exported const array (works for JS too)',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
kind: 'ts-enum',
|
|
88
|
+
extensions: TS_EXTENSIONS,
|
|
89
|
+
pattern: '\\b(?:export\\s+)?(?:const\\s+)?enum\\s+\\w+\\s*\\{',
|
|
90
|
+
flags: 'm',
|
|
91
|
+
reads: 'TS enum members',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
kind: 'ts-object-keys',
|
|
95
|
+
extensions: TS_EXTENSIONS,
|
|
96
|
+
pattern: '\\bexport\\s+(?:const|let)\\s+\\w+\\s*(?::\\s*[^=\\n]+)?=\\s*\\{',
|
|
97
|
+
flags: 'm',
|
|
98
|
+
reads: 'keys of an exported object literal',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
kind: 'ts-union',
|
|
102
|
+
extensions: TS_EXTENSIONS,
|
|
103
|
+
pattern: "\\btype\\s+\\w+\\s*=\\s*[^;\\n]*['\"][^'\"\\n]*['\"]\\s*\\|",
|
|
104
|
+
flags: 'm',
|
|
105
|
+
reads: 'string-literal union members',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
kind: 'yaml-keys',
|
|
109
|
+
extensions: YAML_EXTENSIONS,
|
|
110
|
+
pattern: '^[A-Za-z_][\\w.-]*\\s*:',
|
|
111
|
+
flags: 'm',
|
|
112
|
+
reads: 'top-level keys of a YAML document',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
kind: 'yaml-map-keys',
|
|
116
|
+
extensions: YAML_EXTENSIONS,
|
|
117
|
+
pattern: '^[A-Za-z_][\\w.-]*\\s*:\\s*$',
|
|
118
|
+
flags: 'm',
|
|
119
|
+
reads: 'keys under a dotted path in a YAML document',
|
|
120
|
+
},
|
|
121
|
+
].map((sig) => {
|
|
122
|
+
// Deep freeze: a mutable (or aliased) extensions array would let a push
|
|
123
|
+
// onto one kind silently rewire every kind sharing the same array.
|
|
124
|
+
if (sig.extensions !== null) Object.freeze(sig.extensions);
|
|
125
|
+
return Object.freeze(sig);
|
|
126
|
+
}));
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load a command, then run it through the harness (UCS-956).
|
|
3
|
+
*
|
|
4
|
+
* `runCli` promises that a crash exits 2, never 1. It cannot keep that promise
|
|
5
|
+
* for a failure that happens BEFORE it loads: Node exits 1 on an unhandled ES
|
|
6
|
+
* module load error, so a SyntaxError in any lib/ module — or a missing
|
|
7
|
+
* `js-yaml` — used to make every engine surface exit 1, the FINDINGS code, with
|
|
8
|
+
* the harness never having run.
|
|
9
|
+
*
|
|
10
|
+
* That is the worst possible failure for the exit-code contract (PRD §5,
|
|
11
|
+
* D-011). An agent reads exit 1 as "the check ran and found problems", so it
|
|
12
|
+
* quarantines and continues — past a check that never ran. And it is MORE
|
|
13
|
+
* reachable than an ordinary crash, because the kit is seeded into a client
|
|
14
|
+
* repo (D-001): a partial copy, a corrupted file, or an uninstalled dependency
|
|
15
|
+
* lands exactly here.
|
|
16
|
+
*
|
|
17
|
+
* So the entry point of every surface is a shim that statically imports
|
|
18
|
+
* NOTHING, and reaches the engine only through `import()`, whose failure is an
|
|
19
|
+
* ordinary catchable rejection. Everything the engine is made of — including
|
|
20
|
+
* `cli.js`, and this module — loads inside that seam.
|
|
21
|
+
*
|
|
22
|
+
* This module is loaded dynamically too. If it cannot load, the shim's own
|
|
23
|
+
* catch reports it and exits 2.
|
|
24
|
+
*/
|
|
25
|
+
import process from 'node:process';
|
|
26
|
+
import { runCli } from './cli.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The shim performs the `import()` itself, with a string-literal specifier, and
|
|
30
|
+
* hands the loaded module here. Nothing in the engine ever imports a computed
|
|
31
|
+
* path — there is no variable that could be made to name client code (D-014).
|
|
32
|
+
*
|
|
33
|
+
* @param {string} name the command's name, for its messages
|
|
34
|
+
* @param {{ main: (argv: string[]) => number | Promise<number>, USAGE: string }} command
|
|
35
|
+
* @returns {Promise<number>} an exit code; 1 only if the command returned it
|
|
36
|
+
*/
|
|
37
|
+
export async function boot(name, command) {
|
|
38
|
+
return runCli(name, command.main, { usage: command.USAGE });
|
|
39
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthesized call numbers (UCS-1158) — a display string, and never an identity.
|
|
3
|
+
*
|
|
4
|
+
* A call number is what a browse tree PRINTS beside a leaf so a reader scanning
|
|
5
|
+
* a shelf has a compact handle for where that leaf sits in this particular
|
|
6
|
+
* projection. It is derived, every time, from the leaf's facets. It is not
|
|
7
|
+
* stored, not indexed, not resolvable, and not citable.
|
|
8
|
+
*
|
|
9
|
+
* That last part is the whole reason this module is careful rather than
|
|
10
|
+
* cosmetic. The store this engine serves spent its first life with a Dewey-style
|
|
11
|
+
* dotted notation that fused identity, location and browse order into one
|
|
12
|
+
* string, and the cost of that fusion is what UCS-1147 spent a migration
|
|
13
|
+
* undoing: a citation pointed at a position, so moving a leaf broke every
|
|
14
|
+
* reference to it. A synthesized call number is the SAME SHAPE of string
|
|
15
|
+
* arriving through a different door — positional, human-friendly, and highly
|
|
16
|
+
* tempting to paste into a `see-also`. If that paste ever resolved, the whole
|
|
17
|
+
* inversion would quietly reverse itself one convenient citation at a time.
|
|
18
|
+
*
|
|
19
|
+
* So the defense is structural rather than advisory, in three parts:
|
|
20
|
+
*
|
|
21
|
+
* 1. The grammar cannot be mistaken for an id. `L-000117` is the accession;
|
|
22
|
+
* a call number looks like `DES-COM/REF·L-000117` — uppercase facet
|
|
23
|
+
* abbreviations, a slash, and a MIDDLE DOT that no id grammar accepts.
|
|
24
|
+
* `assertNoIdSpaceMatches` (below) proves it against every compiled id
|
|
25
|
+
* space in ID_GRAMMARS rather than against a remembered list, so a new id
|
|
26
|
+
* space cannot silently start matching call numbers.
|
|
27
|
+
* 2. Nothing accepts one. The citation grammar is `leaf-ref`, accession-only
|
|
28
|
+
* since UCS-1147, so a call number in a citation-shaped field is already a
|
|
29
|
+
* finding — no new check was needed, and tests/derived-call-numbers pins
|
|
30
|
+
* that it stays that way.
|
|
31
|
+
* 3. The accession travels INSIDE the call number, as its suffix. A reader
|
|
32
|
+
* who copies the whole string still carries the identity, and a reader who
|
|
33
|
+
* wants the citable part can see exactly which half it is. The tempting
|
|
34
|
+
* thing to paste therefore contains the correct thing to paste.
|
|
35
|
+
*
|
|
36
|
+
* The call number is NOT stable across projections, and that is a feature: the
|
|
37
|
+
* same leaf reads `DES-COM/REF·L-000117` in the domain-first tree and
|
|
38
|
+
* `REF/DES-COM·L-000117` in the form-first one, because the call number
|
|
39
|
+
* describes a POSITION in a projection, and that leaf holds two of them. A
|
|
40
|
+
* string that changed meaning between trees while looking stable would be the
|
|
41
|
+
* worst of both worlds; one that visibly reorders says what it is.
|
|
42
|
+
*/
|
|
43
|
+
import { idPattern, ID_GRAMMARS } from './id-grammars.js';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The separator between the positional part and the accession.
|
|
47
|
+
*
|
|
48
|
+
* U+00B7 MIDDLE DOT, chosen because no id grammar in this engine accepts it and
|
|
49
|
+
* none plausibly will: the id spaces are all `[A-Z]-[0-9]` or dotted decimals,
|
|
50
|
+
* and a middle dot is not a character an author reaches for when minting an id.
|
|
51
|
+
* It is also visually distinct from the ASCII period that the LEGACY notation
|
|
52
|
+
* used, so `362.1` and `DES-COM/REF·L-000117` cannot be skim-confused.
|
|
53
|
+
*/
|
|
54
|
+
export const CALL_NUMBER_SEPARATOR = '·';
|
|
55
|
+
|
|
56
|
+
/** How many characters of each facet segment survive into the abbreviation. */
|
|
57
|
+
const SEGMENT_WIDTH = 3;
|
|
58
|
+
|
|
59
|
+
/** What a facet segment abbreviates to when it is missing entirely. */
|
|
60
|
+
const ABSENT_SEGMENT = 'UNC';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Abbreviate one facet segment for display.
|
|
64
|
+
*
|
|
65
|
+
* Uppercased and truncated to three characters, with non-alphanumerics dropped
|
|
66
|
+
* so a segment like `components` reads `COM` rather than `COM-`. Deliberately
|
|
67
|
+
* LOSSY: two segments can abbreviate to the same three letters, and that is
|
|
68
|
+
* fine because nothing resolves through this string — the accession suffix is
|
|
69
|
+
* what disambiguates, and it is exact. Making the abbreviation collision-free
|
|
70
|
+
* would mean minting and remembering it, which is precisely the property that
|
|
71
|
+
* would turn a display string back into an identity.
|
|
72
|
+
*
|
|
73
|
+
* An absent or non-string segment reads `UNC` (unclassified) rather than
|
|
74
|
+
* collapsing to an empty string. An empty abbreviation would make
|
|
75
|
+
* `/REF·L-000117` — a call number with a hole where a facet should be, which
|
|
76
|
+
* reads as a formatting bug rather than as the fact that the leaf declares no
|
|
77
|
+
* domain. The tree also demotes such a leaf; the call number says why.
|
|
78
|
+
*
|
|
79
|
+
* @param {unknown} segment one facet path segment
|
|
80
|
+
* @returns {string} the display abbreviation
|
|
81
|
+
*/
|
|
82
|
+
export function abbreviateSegment(segment) {
|
|
83
|
+
if (typeof segment !== 'string') return ABSENT_SEGMENT;
|
|
84
|
+
const cleaned = segment.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
|
|
85
|
+
return cleaned === '' ? ABSENT_SEGMENT : cleaned.slice(0, SEGMENT_WIDTH);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Synthesize the call number for one leaf, in one projection.
|
|
90
|
+
*
|
|
91
|
+
* The positional half is the projection's own path — the same segments the tree
|
|
92
|
+
* nested this leaf under, abbreviated and slash-joined — so the call number and
|
|
93
|
+
* the position it describes cannot disagree: both are built from one array.
|
|
94
|
+
* That is the invariant worth protecting, and it is why this function takes the
|
|
95
|
+
* path rather than re-deriving it from the leaf's facets. A second derivation
|
|
96
|
+
* would be a second chance to differ.
|
|
97
|
+
*
|
|
98
|
+
* The identity half is the accession, verbatim. A leaf that mints none (which
|
|
99
|
+
* the schema refuses, but the derived layer never gates on store health) prints
|
|
100
|
+
* the absent marker instead, so the string stays well-formed and the missing
|
|
101
|
+
* identity is visible rather than swallowed.
|
|
102
|
+
*
|
|
103
|
+
* @param {string[]} path the projection's facet path for this leaf
|
|
104
|
+
* @param {string|null} accession the leaf's accession id, or null
|
|
105
|
+
* @returns {string} the synthesized display string
|
|
106
|
+
*/
|
|
107
|
+
export function synthesizeCallNumber(path, accession) {
|
|
108
|
+
const positional = path.length
|
|
109
|
+
? path.map(abbreviateSegment).join('/')
|
|
110
|
+
: ABSENT_SEGMENT;
|
|
111
|
+
const identity = typeof accession === 'string' && accession !== '' ? accession : ABSENT_SEGMENT;
|
|
112
|
+
return `${positional}${CALL_NUMBER_SEPARATOR}${identity}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Prove a synthesized call number is not an id in ANY space this engine knows.
|
|
117
|
+
*
|
|
118
|
+
* Enumerates ID_GRAMMARS rather than checking a remembered list of spaces, so
|
|
119
|
+
* adding an id space that happened to accept middle dots would fail here at the
|
|
120
|
+
* moment it was added rather than the first time somebody cited a shelf label.
|
|
121
|
+
* Returns the offending spaces rather than throwing: the caller decides whether
|
|
122
|
+
* that is a test failure or a finding, and a library that threw would make the
|
|
123
|
+
* check unusable from the one place it matters most — a test asserting the
|
|
124
|
+
* property holds for every call number a real store generates.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} callNumber a synthesized display string
|
|
127
|
+
* @returns {string[]} the id spaces that would accept it — empty when safe
|
|
128
|
+
*/
|
|
129
|
+
export function idSpacesMatching(callNumber) {
|
|
130
|
+
return Object.keys(ID_GRAMMARS)
|
|
131
|
+
.filter((space) => idPattern(space).test(callNumber))
|
|
132
|
+
.sort();
|
|
133
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI shell, once (UCS-944).
|
|
3
|
+
*
|
|
4
|
+
* Nine command-line surfaces each hand-roll the same flag grammar, the same
|
|
5
|
+
* usage-error type, and the same mapping from outcome to exit code. The copies
|
|
6
|
+
* have already drifted — the `--concepts` filter meant two different things to
|
|
7
|
+
* two validators until UCS-935 settled it.
|
|
8
|
+
*
|
|
9
|
+
* The safety rule they restate is the load-bearing one:
|
|
10
|
+
*
|
|
11
|
+
* EXIT 1 MEANS FINDINGS. A crash must never wear it.
|
|
12
|
+
*
|
|
13
|
+
* An engine that dies mid-check and exits 1 tells an agent riding the exit-code
|
|
14
|
+
* contract that the check RAN and found problems — so it quarantines and
|
|
15
|
+
* continues, past a check that never ran (PRD §5, D-011). Today that rule is
|
|
16
|
+
* enforced by each author remembering to copy a catch block, and this session
|
|
17
|
+
* found FIVE surfaces where the copy was wrong or missing.
|
|
18
|
+
*
|
|
19
|
+
* `runCli` owns the epilogue instead: a usage error reports and exits 2, and
|
|
20
|
+
* ANY unexpected throw reports and exits 2. Exit 1 is reachable only by a main
|
|
21
|
+
* that deliberately returns it, having actually run and actually found things.
|
|
22
|
+
*
|
|
23
|
+
* Every surface reaches this through `lib/boot.js`, from an entry shim that
|
|
24
|
+
* statically imports nothing (UCS-956). The nine hand-written shells that this
|
|
25
|
+
* replaced are gone (UCS-952).
|
|
26
|
+
*/
|
|
27
|
+
import process from 'node:process';
|
|
28
|
+
import { EXIT_CODES } from './exit-codes.js';
|
|
29
|
+
import { UsageError } from './usage-error.js';
|
|
30
|
+
|
|
31
|
+
export { UsageError } from './usage-error.js';
|
|
32
|
+
export { EngineRefusal, rethrowIfBug } from './engine-refusal.js';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse the flag grammar every engine CLI shares.
|
|
36
|
+
*
|
|
37
|
+
* Both conventional spellings work — `--flag value` and `--flag=value` — and
|
|
38
|
+
* everything the grammar cannot account for is a UsageError, never a guess:
|
|
39
|
+
* an unknown flag, a stray positional where none is allowed, a value flag with
|
|
40
|
+
* no value, a boolean flag given one, or an empty value (`--root=` would
|
|
41
|
+
* otherwise resolve to the current directory, answering about a repo nobody
|
|
42
|
+
* named).
|
|
43
|
+
*
|
|
44
|
+
* @param {string[]} argv arguments after the script name
|
|
45
|
+
* @param {object} spec
|
|
46
|
+
* @param {string[]} [spec.boolean] flags that take no value
|
|
47
|
+
* @param {string[]} [spec.value] flags taking one value; last wins
|
|
48
|
+
* @param {string[]} [spec.repeatable] value flags that accumulate into an array
|
|
49
|
+
* @param {string[]} [spec.allowEmpty] value flags for which "" is a legal value
|
|
50
|
+
* — preflight reads an empty `--concepts` as store-health-only (PRD §7)
|
|
51
|
+
* @param {boolean} [spec.positionals] whether bare arguments are allowed
|
|
52
|
+
* @returns {{ options: object, positionals: string[] }}
|
|
53
|
+
*/
|
|
54
|
+
export function parseArgs(argv, spec = {}) {
|
|
55
|
+
const booleans = new Set(spec.boolean ?? []);
|
|
56
|
+
const values = new Set(spec.value ?? []);
|
|
57
|
+
const repeatables = new Set(spec.repeatable ?? []);
|
|
58
|
+
const allowEmpty = new Set(spec.allowEmpty ?? []);
|
|
59
|
+
const known = new Set([...booleans, ...values, ...repeatables]);
|
|
60
|
+
const options = {};
|
|
61
|
+
const positionals = [];
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
64
|
+
const arg = argv[i];
|
|
65
|
+
if (!arg.startsWith('--')) {
|
|
66
|
+
if (!spec.positionals) {
|
|
67
|
+
throw new UsageError(`unexpected argument ${JSON.stringify(arg)} — this CLI takes flags only`);
|
|
68
|
+
}
|
|
69
|
+
positionals.push(arg);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const eq = arg.indexOf('=');
|
|
74
|
+
const flag = eq === -1 ? arg : arg.slice(0, eq);
|
|
75
|
+
const name = flag.slice(2);
|
|
76
|
+
if (!known.has(name)) throw new UsageError(`unknown flag ${flag}`);
|
|
77
|
+
|
|
78
|
+
if (booleans.has(name)) {
|
|
79
|
+
if (eq !== -1) throw new UsageError(`${flag} takes no value`);
|
|
80
|
+
options[name] = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let value;
|
|
85
|
+
if (eq !== -1) {
|
|
86
|
+
// `--flag=value` carries its value unambiguously, so a value that looks
|
|
87
|
+
// like a flag is legal here — this spelling is the escape hatch for one.
|
|
88
|
+
value = arg.slice(eq + 1);
|
|
89
|
+
} else {
|
|
90
|
+
const next = argv[i + 1];
|
|
91
|
+
// In the space form a `--`-prefixed token is the next flag, not a value.
|
|
92
|
+
if (next === undefined || next.startsWith('--')) {
|
|
93
|
+
throw new UsageError(`${flag} requires a value`);
|
|
94
|
+
}
|
|
95
|
+
value = next;
|
|
96
|
+
i += 1;
|
|
97
|
+
}
|
|
98
|
+
// An empty value is as valueless as none, in EITHER spelling. Both `--root=`
|
|
99
|
+
// and `--root ""` arrive when a shell expands an unset variable, and both
|
|
100
|
+
// would otherwise resolve to the current directory — answering about a repo
|
|
101
|
+
// nobody named.
|
|
102
|
+
// A flag may declare "" meaningful — preflight's empty `--concepts` selects
|
|
103
|
+
// store-health-only. Everything else treats it as no value at all.
|
|
104
|
+
if (value === '' && !allowEmpty.has(name)) throw new UsageError(`${flag} requires a value`);
|
|
105
|
+
|
|
106
|
+
if (repeatables.has(name)) (options[name] ??= []).push(value);
|
|
107
|
+
else options[name] = value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { options, positionals };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Run a command's main and own the epilogue: nothing it throws can exit 1.
|
|
115
|
+
*
|
|
116
|
+
* `main` returns an exit code, or throws. A UsageError reports itself with the
|
|
117
|
+
* usage line; anything else is an engine failure — the command did not finish,
|
|
118
|
+
* so it cannot have findings. Both exit 2.
|
|
119
|
+
*
|
|
120
|
+
* Returns the code rather than setting it, so the caller stays in charge of
|
|
121
|
+
* `process.exitCode`. Never calls `process.exit()`: that drops queued async
|
|
122
|
+
* stdout writes, truncating piped `--json` at the pipe buffer and yielding
|
|
123
|
+
* corrupt JSON with a clean exit code.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} name the command's name, for its messages
|
|
126
|
+
* @param {(argv: string[]) => number | Promise<number>} main
|
|
127
|
+
* @param {object} opts
|
|
128
|
+
* @param {string} opts.usage the usage line printed on a UsageError
|
|
129
|
+
* @param {string[]} [opts.argv]
|
|
130
|
+
* @param {{ write: (s: string) => unknown }} [opts.stderr]
|
|
131
|
+
* @returns {Promise<number>} an exit code; 1 only if `main` returned it
|
|
132
|
+
*/
|
|
133
|
+
export async function runCli(name, main, { usage, argv = process.argv.slice(2), stderr = process.stderr } = {}) {
|
|
134
|
+
try {
|
|
135
|
+
return await main(argv);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error instanceof UsageError) {
|
|
138
|
+
stderr.write(`${name}: ${error.message}\n${usage}\n`);
|
|
139
|
+
return EXIT_CODES.FAILURE;
|
|
140
|
+
}
|
|
141
|
+
// The command did not finish. It cannot have findings, so it must not wear
|
|
142
|
+
// the FINDINGS code — an agent would quarantine-and-continue past a check
|
|
143
|
+
// that never ran.
|
|
144
|
+
stderr.write(`${name}: internal failure — the command did not complete\n${error?.stack || error?.message || String(error)}\n`);
|
|
145
|
+
return EXIT_CODES.FAILURE;
|
|
146
|
+
}
|
|
147
|
+
}
|