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,955 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extractor-kind registry (KK-07 frame; KK-08 TS/JS kinds; KK-09 Swift +
|
|
3
|
+
* config kinds; KK-10 dir-modules). Each kind is a
|
|
4
|
+
* small deterministic recipe `extract(text, descriptor) -> string[]` that
|
|
5
|
+
* re-derives a claimed value set from a reified anchor. The contract
|
|
6
|
+
* (payload/templates/new-kind/parser.example.js teaches it):
|
|
7
|
+
*
|
|
8
|
+
* 1. PURE + DETERMINISTIC — source text in, value set out. Lexical parsing
|
|
9
|
+
* only: the engine never executes client code (D-014).
|
|
10
|
+
* 2. HARD-ERROR, NEVER GUESS — anything the recipe cannot parse throws,
|
|
11
|
+
* never a skipped span (PRD §5).
|
|
12
|
+
* 3. DECLARED SYNTACTIC ENVELOPE — out-of-envelope sentinels in the matched
|
|
13
|
+
* span (TS spread / computed keys / re-exports, template interpolation,
|
|
14
|
+
* escape sequences) throw EnvelopeError: a confident wrong parse is a
|
|
15
|
+
* false all-clear, the D-005/D-012 failure class.
|
|
16
|
+
* 4. VALUES ARE STRINGS, byte-exact, case-sensitive, as sets (§3.5) —
|
|
17
|
+
* duplicates are emitted as read; the caller's diff makes the finding.
|
|
18
|
+
*
|
|
19
|
+
* Facets: each kind documents which facet of the anchor it emits. Where an
|
|
20
|
+
* anchor legitimately carries two (ts-enum member names vs raw values), the
|
|
21
|
+
* descriptor pins one with `emit:`.
|
|
22
|
+
*
|
|
23
|
+
* Parsing is regex-level by design (PRD §5.1): the TS and Swift kinds share
|
|
24
|
+
* tiny string/comment-aware token walks — no AST, no resolver, single file
|
|
25
|
+
* only. The config kinds (json-*, yaml-*, strings-keys/.xcstrings) parse
|
|
26
|
+
* DATA with a real data parser (JSON.parse / js-yaml `load`) — data, never
|
|
27
|
+
* code, the same D-014 line.
|
|
28
|
+
*
|
|
29
|
+
* DISPATCH SEAM (KK-10): a registry value is either a plain FUNCTION — a
|
|
30
|
+
* file kind, `extract(text, descriptor)` over the source file's text — or a
|
|
31
|
+
* frozen OBJECT `{ reads: 'directory', extract(entries, descriptor) }` — a
|
|
32
|
+
* directory kind, whose input is a deterministic directory listing the
|
|
33
|
+
* CALLER produces (listDirectory below: readdirSync withFileTypes, entries
|
|
34
|
+
* as `{ name, isDirectory, isSymbolicLink }`, sorted by name). The recipes
|
|
35
|
+
* themselves stay pure either way: listing in, value set out — the caller
|
|
36
|
+
* (validate-values.js checkDescriptor) owns the one filesystem read, exactly
|
|
37
|
+
* as it owns readFileSync for file kinds.
|
|
38
|
+
*/
|
|
39
|
+
import { readdirSync } from 'node:fs';
|
|
40
|
+
import { load as yamlLoad } from 'js-yaml';
|
|
41
|
+
import { compare } from './validate-record.js';
|
|
42
|
+
|
|
43
|
+
/** The matched span contains a sentinel the kind's grammar cannot see past. */
|
|
44
|
+
export class EnvelopeError extends Error {}
|
|
45
|
+
/** The recipe could not read a value set out of the source at all. */
|
|
46
|
+
export class ExtractError extends Error {}
|
|
47
|
+
|
|
48
|
+
// ------------------------------------------------------------- token walking
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Lexically tokenize a TS/JS span into strings, identifiers, numbers, and
|
|
52
|
+
* punctuation, skipping comments. String tokens carry decoded === raw source
|
|
53
|
+
* bytes between the quotes: a backslash escape means the runtime value's
|
|
54
|
+
* bytes differ from their source spelling, so the claim could never be
|
|
55
|
+
* byte-matched honestly (§3.5) — that is an out-of-envelope sentinel, not a
|
|
56
|
+
* decoding exercise. Template literals with `${` interpolation are sentinels
|
|
57
|
+
* for the same reason: the value is not lexically knowable.
|
|
58
|
+
*/
|
|
59
|
+
function tokenize(span, context) {
|
|
60
|
+
const tokens = [];
|
|
61
|
+
let i = 0;
|
|
62
|
+
while (i < span.length) {
|
|
63
|
+
const ch = span[i];
|
|
64
|
+
if (/\s/.test(ch)) { i += 1; continue; }
|
|
65
|
+
if (ch === '/' && span[i + 1] === '/') {
|
|
66
|
+
const nl = span.indexOf('\n', i);
|
|
67
|
+
i = nl === -1 ? span.length : nl + 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (ch === '/' && span[i + 1] === '*') {
|
|
71
|
+
const end = span.indexOf('*/', i + 2);
|
|
72
|
+
if (end === -1) throw new ExtractError(`${context}: unterminated block comment`);
|
|
73
|
+
i = end + 2;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
77
|
+
let j = i + 1;
|
|
78
|
+
while (j < span.length && span[j] !== ch) {
|
|
79
|
+
if (span[j] === '\\') {
|
|
80
|
+
throw new EnvelopeError(`${context}: string literal carries an escape sequence (${JSON.stringify(span.slice(i, j + 2))}…) — its runtime bytes differ from their source spelling, so the value could never byte-match its claim (§3.5); out of this kind's envelope`);
|
|
81
|
+
}
|
|
82
|
+
if (ch === '`' && span[j] === '$' && span[j + 1] === '{') {
|
|
83
|
+
throw new EnvelopeError(`${context}: template literal interpolation ("\${") — the value is not lexically knowable; a confident wrong parse is a false all-clear (PRD §5.1)`);
|
|
84
|
+
}
|
|
85
|
+
j += 1;
|
|
86
|
+
}
|
|
87
|
+
if (j >= span.length) throw new ExtractError(`${context}: unterminated string literal`);
|
|
88
|
+
tokens.push({ type: 'string', value: span.slice(i + 1, j) });
|
|
89
|
+
i = j + 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (/[A-Za-z_$]/.test(ch)) {
|
|
93
|
+
let j = i + 1;
|
|
94
|
+
while (j < span.length && /[\w$]/.test(span[j])) j += 1;
|
|
95
|
+
tokens.push({ type: 'ident', value: span.slice(i, j) });
|
|
96
|
+
i = j;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (/[0-9]/.test(ch) || (ch === '.' && /[0-9]/.test(span[i + 1] ?? ''))) {
|
|
100
|
+
let j = i + 1;
|
|
101
|
+
while (j < span.length && /[\w.]/.test(span[j])) j += 1;
|
|
102
|
+
tokens.push({ type: 'number', value: span.slice(i, j) });
|
|
103
|
+
i = j;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (ch === '.' && span[i + 1] === '.' && span[i + 2] === '.') {
|
|
107
|
+
tokens.push({ type: 'punct', value: '...' });
|
|
108
|
+
i += 3;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
tokens.push({ type: 'punct', value: ch });
|
|
112
|
+
i += 1;
|
|
113
|
+
}
|
|
114
|
+
return tokens;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Walk from an opening delimiter to its balanced close, string/comment-aware,
|
|
119
|
+
* returning the inner span. `start` indexes the opening delimiter itself.
|
|
120
|
+
*/
|
|
121
|
+
function balancedSpan(text, start, open, close, context) {
|
|
122
|
+
let depth = 0;
|
|
123
|
+
let i = start;
|
|
124
|
+
while (i < text.length) {
|
|
125
|
+
const ch = text[i];
|
|
126
|
+
if (ch === '/' && text[i + 1] === '/') {
|
|
127
|
+
const nl = text.indexOf('\n', i);
|
|
128
|
+
i = nl === -1 ? text.length : nl + 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (ch === '/' && text[i + 1] === '*') {
|
|
132
|
+
const end = text.indexOf('*/', i + 2);
|
|
133
|
+
if (end === -1) throw new ExtractError(`${context}: unterminated block comment`);
|
|
134
|
+
i = end + 2;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
138
|
+
let j = i + 1;
|
|
139
|
+
while (j < text.length && text[j] !== ch) {
|
|
140
|
+
if (text[j] === '\\') j += 1; // span walk only; escapes sentinel later
|
|
141
|
+
j += 1;
|
|
142
|
+
}
|
|
143
|
+
if (j >= text.length) throw new ExtractError(`${context}: unterminated string literal`);
|
|
144
|
+
i = j + 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (ch === open) depth += 1;
|
|
148
|
+
else if (ch === close) {
|
|
149
|
+
depth -= 1;
|
|
150
|
+
if (depth === 0) return text.slice(start + 1, i);
|
|
151
|
+
}
|
|
152
|
+
i += 1;
|
|
153
|
+
}
|
|
154
|
+
throw new ExtractError(`${context}: unbalanced ${JSON.stringify(open)}…${JSON.stringify(close)} — the declaration never closes`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A symbol that is only RE-EXPORTED here is an out-of-envelope sentinel, not
|
|
161
|
+
* a miss: parsing is lexical and single-file, so following the chain would be
|
|
162
|
+
* resolution, and extracting nothing would be a confident wrong parse.
|
|
163
|
+
*/
|
|
164
|
+
function requireDeclaration(text, symbol, kind, matched) {
|
|
165
|
+
if (matched !== null) return matched;
|
|
166
|
+
const reExport = new RegExp(`export\\s*(?:type\\s*)?\\{[^}]*\\b${escapeRegExp(symbol)}\\b[^}]*\\}\\s*from\\b`);
|
|
167
|
+
if (reExport.test(text) || /export\s*\*\s*from\b/.test(text)) {
|
|
168
|
+
throw new EnvelopeError(`"${symbol}" is not declared in this file — it is (or may be) re-exported from another module, and ${kind} parses lexically, single-file only (PRD §5.1): resolving the chain is out of the envelope`);
|
|
169
|
+
}
|
|
170
|
+
throw new ExtractError(`no ${kind} declaration of "${symbol}" found — wrong pointer, or the anchor is no longer reified`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Require the descriptor's `symbol:` — these kinds anchor to a declaration. */
|
|
174
|
+
function requireSymbol(descriptor, kind) {
|
|
175
|
+
const symbol = descriptor.symbol;
|
|
176
|
+
if (typeof symbol !== 'string' || symbol === '') {
|
|
177
|
+
throw new ExtractError(`${kind} requires a "symbol:" naming the declaration to read`);
|
|
178
|
+
}
|
|
179
|
+
return symbol;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Find `export const <symbol>` and the value expression opener after `=`. */
|
|
183
|
+
function exportedConstOpener(text, symbol, kind) {
|
|
184
|
+
const decl = new RegExp(`(?:^|[\\n;])\\s*export\\s+const\\s+${escapeRegExp(symbol)}\\b`).exec(text);
|
|
185
|
+
requireDeclaration(text, symbol, kind, decl);
|
|
186
|
+
const eq = text.indexOf('=', decl.index + decl[0].length);
|
|
187
|
+
if (eq === -1) throw new ExtractError(`declaration of "${symbol}" carries no initializer — nothing to extract`);
|
|
188
|
+
let i = eq + 1;
|
|
189
|
+
while (i < text.length && /\s/.test(text[i])) i += 1;
|
|
190
|
+
return { opener: text[i], at: i };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ------------------------------------------------------------------ the kinds
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* `ts-const-array` — string elements of an exported `const` array literal
|
|
197
|
+
* (facet: the element strings). Works for plain JS too: kinds describe
|
|
198
|
+
* declaration shape, not file type. Envelope: string literals, commas, and
|
|
199
|
+
* comments inside the brackets; spread (`...`) and any non-string member are
|
|
200
|
+
* sentinels — a spread hides members no lexical parse can see.
|
|
201
|
+
*/
|
|
202
|
+
function tsConstArray(text, descriptor) {
|
|
203
|
+
const symbol = requireSymbol(descriptor, 'ts-const-array');
|
|
204
|
+
const { opener, at } = exportedConstOpener(text, symbol, 'ts-const-array');
|
|
205
|
+
if (opener !== '[') {
|
|
206
|
+
throw new ExtractError(`"${symbol}" is not initialized with an array literal — out of ts-const-array's reach`);
|
|
207
|
+
}
|
|
208
|
+
const tokens = tokenize(balancedSpan(text, at, '[', ']', symbol), symbol);
|
|
209
|
+
const values = [];
|
|
210
|
+
for (const t of tokens) {
|
|
211
|
+
if (t.type === 'string') { values.push(t.value); continue; }
|
|
212
|
+
if (t.type === 'punct' && t.value === ',') continue;
|
|
213
|
+
if (t.type === 'punct' && t.value === '...') {
|
|
214
|
+
throw new EnvelopeError(`"${symbol}" spreads another array ("...") — the full member set is not lexically knowable; extracting the literal members would be a confident wrong parse (PRD §5.1)`);
|
|
215
|
+
}
|
|
216
|
+
throw new EnvelopeError(`"${symbol}" carries a non-string-literal member (${JSON.stringify(t.value)}) — outside the ts-const-array envelope (string literals and commas only)`);
|
|
217
|
+
}
|
|
218
|
+
return values;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* `ts-union` — string-literal members of a `type X = 'a' | 'b'` union
|
|
223
|
+
* (facet: the member strings). Envelope: string literals joined by `|`,
|
|
224
|
+
* leading pipe and interleaved comments allowed; any identifier member is a
|
|
225
|
+
* type reference this single-file grammar cannot resolve — sentinel. A
|
|
226
|
+
* symbol that is only re-exported here is a sentinel, never followed.
|
|
227
|
+
*/
|
|
228
|
+
function tsUnion(text, descriptor) {
|
|
229
|
+
const symbol = requireSymbol(descriptor, 'ts-union');
|
|
230
|
+
const decl = new RegExp(`(?:^|[\\n;])\\s*(?:export\\s+)?type\\s+${escapeRegExp(symbol)}\\s*=`).exec(text);
|
|
231
|
+
requireDeclaration(text, symbol, 'ts-union', decl);
|
|
232
|
+
const start = decl.index + decl[0].length;
|
|
233
|
+
// The union ends at the first `;` outside strings/comments (or EOF).
|
|
234
|
+
let end = text.length;
|
|
235
|
+
for (let i = start; i < text.length; i += 1) {
|
|
236
|
+
const ch = text[i];
|
|
237
|
+
if (ch === '/' && text[i + 1] === '/') { const nl = text.indexOf('\n', i); i = nl === -1 ? text.length : nl; continue; }
|
|
238
|
+
if (ch === '/' && text[i + 1] === '*') { const e = text.indexOf('*/', i + 2); if (e === -1) throw new ExtractError(`${symbol}: unterminated block comment`); i = e + 1; continue; }
|
|
239
|
+
if (ch === "'" || ch === '"' || ch === '`') { let j = i + 1; while (j < text.length && text[j] !== ch) j += text[j] === '\\' ? 2 : 1; i = j; continue; }
|
|
240
|
+
if (ch === ';') { end = i; break; }
|
|
241
|
+
}
|
|
242
|
+
const tokens = tokenize(text.slice(start, end), symbol);
|
|
243
|
+
const values = [];
|
|
244
|
+
let expectValue = true;
|
|
245
|
+
for (const t of tokens) {
|
|
246
|
+
if (t.type === 'punct' && t.value === '|') { expectValue = true; continue; }
|
|
247
|
+
if (t.type === 'string' && expectValue) { values.push(t.value); expectValue = false; continue; }
|
|
248
|
+
throw new EnvelopeError(`type "${symbol}" carries a non-string-literal union member (${JSON.stringify(t.value)}) — a type reference or operator this lexical, single-file grammar cannot resolve; outside the ts-union envelope`);
|
|
249
|
+
}
|
|
250
|
+
if (values.length === 0) {
|
|
251
|
+
throw new ExtractError(`type "${symbol}" has no string-literal members — nothing ts-union can extract`);
|
|
252
|
+
}
|
|
253
|
+
return values;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* `ts-enum` — members of a TS `enum` declaration. TWO legitimate facets:
|
|
258
|
+
* member NAMES and raw string VALUES; the descriptor pins one with
|
|
259
|
+
* `emit: names` (the default) or `emit: values` (§3.5). Envelope: plain or
|
|
260
|
+
* string-literal member names, optional string initializers; computed names
|
|
261
|
+
* and non-string initializers under `emit: values` are sentinels.
|
|
262
|
+
*/
|
|
263
|
+
function tsEnum(text, descriptor) {
|
|
264
|
+
const symbol = requireSymbol(descriptor, 'ts-enum');
|
|
265
|
+
const emit = descriptor.emit ?? 'names';
|
|
266
|
+
if (emit !== 'names' && emit !== 'values') {
|
|
267
|
+
throw new ExtractError(`ts-enum emits "names" or "values" — descriptor says emit: ${JSON.stringify(descriptor.emit)}; pin one legitimate facet (§3.5)`);
|
|
268
|
+
}
|
|
269
|
+
const decl = new RegExp(`(?:^|[\\n;])\\s*(?:export\\s+)?(?:declare\\s+)?(?:const\\s+)?enum\\s+${escapeRegExp(symbol)}\\s*\\{`).exec(text);
|
|
270
|
+
requireDeclaration(text, symbol, 'ts-enum', decl);
|
|
271
|
+
const open = decl.index + decl[0].length - 1;
|
|
272
|
+
const tokens = tokenize(balancedSpan(text, open, '{', '}', symbol), symbol);
|
|
273
|
+
const values = [];
|
|
274
|
+
let i = 0;
|
|
275
|
+
while (i < tokens.length) {
|
|
276
|
+
const name = tokens[i];
|
|
277
|
+
if (!(name.type === 'ident' || name.type === 'string')) {
|
|
278
|
+
throw new EnvelopeError(`enum "${symbol}" carries a computed or unreadable member name (${JSON.stringify(name.value)}) — outside the ts-enum envelope`);
|
|
279
|
+
}
|
|
280
|
+
i += 1;
|
|
281
|
+
let initializer = null;
|
|
282
|
+
if (tokens[i]?.type === 'punct' && tokens[i].value === '=') {
|
|
283
|
+
initializer = tokens[i + 1];
|
|
284
|
+
if (!initializer || initializer.type !== 'string' || (tokens[i + 2] && !(tokens[i + 2].type === 'punct' && tokens[i + 2].value === ','))) {
|
|
285
|
+
throw new EnvelopeError(`enum "${symbol}" member ${JSON.stringify(name.value)} has a non-string or computed initializer — its value is not lexically knowable; outside the ts-enum envelope`);
|
|
286
|
+
}
|
|
287
|
+
i += 2;
|
|
288
|
+
}
|
|
289
|
+
if (emit === 'values') {
|
|
290
|
+
if (!initializer) {
|
|
291
|
+
throw new EnvelopeError(`enum "${symbol}" member ${JSON.stringify(name.value)} has no string initializer, but the descriptor pins emit: values — the raw value (TS auto-numbers) is not a string set (§3.5)`);
|
|
292
|
+
}
|
|
293
|
+
values.push(initializer.value);
|
|
294
|
+
} else {
|
|
295
|
+
values.push(name.value);
|
|
296
|
+
}
|
|
297
|
+
if (tokens[i]?.type === 'punct' && tokens[i].value === ',') i += 1;
|
|
298
|
+
else if (i < tokens.length) {
|
|
299
|
+
throw new EnvelopeError(`enum "${symbol}": unexpected ${JSON.stringify(tokens[i].value)} after member ${JSON.stringify(name.value)} — outside the ts-enum envelope`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (values.length === 0) throw new ExtractError(`enum "${symbol}" has no members — nothing to extract`);
|
|
303
|
+
return values;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* `ts-object-keys` — top-level keys of an exported object literal (facet:
|
|
308
|
+
* the key strings; nested objects/arrays are values, never keys). Reads
|
|
309
|
+
* .ts/.tsx/.js/.jsx alike — only the named symbol's span counts, so JSX
|
|
310
|
+
* inline object literals elsewhere never match. Envelope: bare or quoted
|
|
311
|
+
* keys with `:` values, shorthand properties; computed keys (`[expr]`) and
|
|
312
|
+
* spread at the top level are sentinels — the key set would not be lexically
|
|
313
|
+
* knowable.
|
|
314
|
+
*/
|
|
315
|
+
function tsObjectKeys(text, descriptor) {
|
|
316
|
+
const symbol = requireSymbol(descriptor, 'ts-object-keys');
|
|
317
|
+
const { opener, at } = exportedConstOpener(text, symbol, 'ts-object-keys');
|
|
318
|
+
if (opener !== '{') {
|
|
319
|
+
throw new ExtractError(`"${symbol}" is not initialized with an object literal — out of ts-object-keys's reach`);
|
|
320
|
+
}
|
|
321
|
+
const tokens = tokenize(balancedSpan(text, at, '{', '}', symbol), symbol);
|
|
322
|
+
const values = [];
|
|
323
|
+
let i = 0;
|
|
324
|
+
while (i < tokens.length) {
|
|
325
|
+
const key = tokens[i];
|
|
326
|
+
if (key.type === 'punct' && key.value === '[') {
|
|
327
|
+
throw new EnvelopeError(`"${symbol}" carries a computed key ("[…]") — the key set is not lexically knowable; extracting the literal keys would be a confident wrong parse (PRD §5.1)`);
|
|
328
|
+
}
|
|
329
|
+
if (key.type === 'punct' && key.value === '...') {
|
|
330
|
+
throw new EnvelopeError(`"${symbol}" spreads another object ("...") — the full key set is not lexically knowable (PRD §5.1)`);
|
|
331
|
+
}
|
|
332
|
+
if (!(key.type === 'ident' || key.type === 'string')) {
|
|
333
|
+
throw new EnvelopeError(`"${symbol}" carries an unreadable key (${JSON.stringify(key.value)}) — outside the ts-object-keys envelope`);
|
|
334
|
+
}
|
|
335
|
+
values.push(key.value);
|
|
336
|
+
i += 1;
|
|
337
|
+
const next = tokens[i];
|
|
338
|
+
if (!next || (next.type === 'punct' && next.value === ',')) { i += 1; continue; } // shorthand
|
|
339
|
+
if (!(next.type === 'punct' && next.value === ':')) {
|
|
340
|
+
throw new EnvelopeError(`"${symbol}" key ${JSON.stringify(key.value)} is not a plain \`key: value\` or shorthand property — outside the ts-object-keys envelope`);
|
|
341
|
+
}
|
|
342
|
+
// Skip the value: walk to the next comma at depth 0, tracking nesting.
|
|
343
|
+
i += 1;
|
|
344
|
+
let depth = 0;
|
|
345
|
+
while (i < tokens.length) {
|
|
346
|
+
const t = tokens[i];
|
|
347
|
+
if (t.type === 'punct' && '{[('.includes(t.value)) depth += 1;
|
|
348
|
+
else if (t.type === 'punct' && '}])'.includes(t.value)) depth -= 1;
|
|
349
|
+
else if (t.type === 'punct' && t.value === ',' && depth === 0) break;
|
|
350
|
+
i += 1;
|
|
351
|
+
}
|
|
352
|
+
i += 1; // past the comma (or EOF: trailing comma optional)
|
|
353
|
+
}
|
|
354
|
+
if (values.length === 0) throw new ExtractError(`"${symbol}" has no keys — nothing to extract`);
|
|
355
|
+
return values;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Shared JSON body: parse (JSON.parse is lexical — data, never code),
|
|
360
|
+
* navigate the optional dotted path, and return the target object's keys
|
|
361
|
+
* (facet: key strings). RFC 8259 duplicate keys collapse last-wins inside
|
|
362
|
+
* JSON.parse, so a duplicated key is invisible to these kinds — JSON is the
|
|
363
|
+
* one grammar here where the parse itself defines away the duplicate.
|
|
364
|
+
*/
|
|
365
|
+
function jsonKeysAt(text, path, kind) {
|
|
366
|
+
let data;
|
|
367
|
+
try {
|
|
368
|
+
data = JSON.parse(text);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
throw new ExtractError(`source is not valid JSON: ${error.message}`);
|
|
371
|
+
}
|
|
372
|
+
let target = data;
|
|
373
|
+
const crumbs = [];
|
|
374
|
+
for (const step of path) {
|
|
375
|
+
crumbs.push(step);
|
|
376
|
+
if (typeof target !== 'object' || target === null || Array.isArray(target) || !Object.hasOwn(target, step)) {
|
|
377
|
+
throw new ExtractError(`path "${crumbs.join('.')}" not found in the JSON document — wrong pointer, or the block moved`);
|
|
378
|
+
}
|
|
379
|
+
target = target[step];
|
|
380
|
+
}
|
|
381
|
+
if (typeof target !== 'object' || target === null || Array.isArray(target)) {
|
|
382
|
+
throw new ExtractError(`${kind} needs an object at ${path.length ? `path "${path.join('.')}"` : 'the top level'}, found ${Array.isArray(target) ? 'an array' : JSON.stringify(target === null ? null : typeof target)}`);
|
|
383
|
+
}
|
|
384
|
+
return Object.keys(target);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** `json-keys` — top-level keys of a JSON document (facet: key strings). */
|
|
388
|
+
function jsonKeys(text) {
|
|
389
|
+
return jsonKeysAt(text, [], 'json-keys');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* `json-map-keys` — keys of the object at the descriptor's dotted `symbol:`
|
|
394
|
+
* path (e.g. `symbol: dependencies` in package.json). Facet: key strings.
|
|
395
|
+
*/
|
|
396
|
+
function jsonMapKeys(text, descriptor) {
|
|
397
|
+
const symbol = requireSymbol(descriptor, 'json-map-keys');
|
|
398
|
+
return jsonKeysAt(text, symbol.split('.'), 'json-map-keys');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// -------------------------------------------------------- Swift token walking
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Swift comments differ from TS just enough to need their own skipper: block
|
|
405
|
+
* comments NEST (a "/*" inside a block comment opens another level), and "'"
|
|
406
|
+
* is not a string delimiter. Returns the index just past the comment starting
|
|
407
|
+
* at `i`, or `i` itself when nothing comment-like starts there.
|
|
408
|
+
*/
|
|
409
|
+
function skipSwiftComment(text, i, context) {
|
|
410
|
+
if (text[i] !== '/' || (text[i + 1] !== '/' && text[i + 1] !== '*')) return i;
|
|
411
|
+
if (text[i + 1] === '/') {
|
|
412
|
+
const nl = text.indexOf('\n', i);
|
|
413
|
+
return nl === -1 ? text.length : nl + 1;
|
|
414
|
+
}
|
|
415
|
+
let depth = 0;
|
|
416
|
+
let j = i;
|
|
417
|
+
while (j < text.length) {
|
|
418
|
+
if (text[j] === '/' && text[j + 1] === '*') { depth += 1; j += 2; continue; }
|
|
419
|
+
if (text[j] === '*' && text[j + 1] === '/') {
|
|
420
|
+
depth -= 1;
|
|
421
|
+
j += 2;
|
|
422
|
+
if (depth === 0) return j;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
j += 1;
|
|
426
|
+
}
|
|
427
|
+
throw new ExtractError(`${context}: unterminated block comment`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* A `#` directive (`#if` conditional compilation and friends) anywhere in a
|
|
432
|
+
* matched Swift span is THE declared out-of-envelope sentinel (§5.1): the
|
|
433
|
+
* value set depends on build configuration no lexical parse can evaluate —
|
|
434
|
+
* a confident parse would silently include or exclude members (D-005/D-012).
|
|
435
|
+
*/
|
|
436
|
+
function swiftDirectiveSentinel(text, i, context) {
|
|
437
|
+
if (text[i] === '#' && /[A-Za-z]/.test(text[i + 1] ?? '')) {
|
|
438
|
+
const word = /#[A-Za-z]+/.exec(text.slice(i))[0];
|
|
439
|
+
throw new EnvelopeError(`${context}: compiler directive "${word}" inside the matched span — conditional compilation makes the value set depend on build configuration, which no lexical parse can evaluate; a confident wrong parse is a false all-clear (PRD §5.1)`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Read the Swift string literal opening at `i` (`text[i] === '"'`) and return
|
|
445
|
+
* `{ value, end }` (end = index past the closing quote). Escape sequences and
|
|
446
|
+
* `\(...)` interpolation are out-of-envelope sentinels — the runtime bytes
|
|
447
|
+
* would differ from (or not be derivable from) their source spelling, so the
|
|
448
|
+
* claim could never byte-match honestly (§3.5). Multiline `"""` literals are
|
|
449
|
+
* sentinels for the same reason (their indentation stripping is semantics).
|
|
450
|
+
*/
|
|
451
|
+
function swiftStringValue(text, i, context) {
|
|
452
|
+
if (text[i + 1] === '"' && text[i + 2] === '"') {
|
|
453
|
+
throw new EnvelopeError(`${context}: multiline string literal ('"""') — its runtime bytes depend on indentation stripping, not source spelling alone; out of this kind's envelope (§3.5)`);
|
|
454
|
+
}
|
|
455
|
+
let j = i + 1;
|
|
456
|
+
while (j < text.length && text[j] !== '"') {
|
|
457
|
+
if (text[j] === '\\') {
|
|
458
|
+
if (text[j + 1] === '(') {
|
|
459
|
+
throw new EnvelopeError(`${context}: string interpolation ("\\(") — the value is not lexically knowable; a confident wrong parse is a false all-clear (PRD §5.1)`);
|
|
460
|
+
}
|
|
461
|
+
throw new EnvelopeError(`${context}: string literal carries an escape sequence (${JSON.stringify(text.slice(i, j + 2))}…) — its runtime bytes differ from their source spelling, so the value could never byte-match its claim (§3.5); out of this kind's envelope`);
|
|
462
|
+
}
|
|
463
|
+
if (text[j] === '\n') throw new ExtractError(`${context}: unterminated string literal`);
|
|
464
|
+
j += 1;
|
|
465
|
+
}
|
|
466
|
+
if (j >= text.length) throw new ExtractError(`${context}: unterminated string literal`);
|
|
467
|
+
return { value: text.slice(i + 1, j), end: j + 1 };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Walk from an opening delimiter to its balanced close in SWIFT text (nested
|
|
472
|
+
* block comments, `"` strings only), returning `{ inner, end }` where `end`
|
|
473
|
+
* indexes the closing delimiter. Escapes inside strings are skipped at walk
|
|
474
|
+
* level only — whether they sentinel is the value reader's call.
|
|
475
|
+
*/
|
|
476
|
+
function swiftBalancedSpan(text, start, open, close, context) {
|
|
477
|
+
let depth = 0;
|
|
478
|
+
let i = start;
|
|
479
|
+
while (i < text.length) {
|
|
480
|
+
const skipped = skipSwiftComment(text, i, context);
|
|
481
|
+
if (skipped !== i) { i = skipped; continue; }
|
|
482
|
+
const ch = text[i];
|
|
483
|
+
if (ch === '"') {
|
|
484
|
+
let j = i + 1;
|
|
485
|
+
while (j < text.length && text[j] !== '"') j += text[j] === '\\' ? 2 : 1;
|
|
486
|
+
if (j >= text.length) throw new ExtractError(`${context}: unterminated string literal`);
|
|
487
|
+
i = j + 1;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (ch === open) depth += 1;
|
|
491
|
+
else if (ch === close) {
|
|
492
|
+
depth -= 1;
|
|
493
|
+
if (depth === 0) return { inner: text.slice(start + 1, i), end: i };
|
|
494
|
+
}
|
|
495
|
+
i += 1;
|
|
496
|
+
}
|
|
497
|
+
throw new ExtractError(`${context}: unbalanced ${JSON.stringify(open)}…${JSON.stringify(close)} — the declaration never closes`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Index of the next non-whitespace, non-comment character at/after `i`. */
|
|
501
|
+
function skipSwiftTrivia(text, i, context) {
|
|
502
|
+
while (i < text.length) {
|
|
503
|
+
if (/\s/.test(text[i])) { i += 1; continue; }
|
|
504
|
+
const skipped = skipSwiftComment(text, i, context);
|
|
505
|
+
if (skipped !== i) { i = skipped; continue; }
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
return i;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ------------------------------------------------------------ the Swift kinds
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* `swift-enum` — cases of a Swift `enum` declaration. TWO legitimate facets
|
|
515
|
+
* (mirroring ts-enum): case NAMES and raw string VALUES; the descriptor pins
|
|
516
|
+
* one with `emit: case-name` (the default) or `emit: raw-value` (§3.5).
|
|
517
|
+
* Envelope: plain cases, comma-joined case lists (`case a, b, c`), optional
|
|
518
|
+
* string raw values, interior comments, nested member bodies (a `switch`'s
|
|
519
|
+
* pattern arms sit at brace depth > 0 and are never counted). Sentinels:
|
|
520
|
+
* `#if` conditional compilation anywhere in the matched span, associated
|
|
521
|
+
* values (`case foo(Int)` — the value set is a payload type, not a string
|
|
522
|
+
* set), backtick-escaped case names (their runtime spelling drops the
|
|
523
|
+
* backticks — not byte-matchable), non-string raw values, and — under
|
|
524
|
+
* `emit: raw-value` — a case with NO explicit raw value: Swift derives the
|
|
525
|
+
* implicit raw value from the inheritance clause's semantics, which this
|
|
526
|
+
* lexical, single-file grammar refuses to evaluate.
|
|
527
|
+
*/
|
|
528
|
+
function swiftEnum(text, descriptor) {
|
|
529
|
+
const symbol = requireSymbol(descriptor, 'swift-enum');
|
|
530
|
+
const emit = descriptor.emit ?? 'case-name';
|
|
531
|
+
if (emit !== 'case-name' && emit !== 'raw-value') {
|
|
532
|
+
throw new ExtractError(`swift-enum emits "case-name" or "raw-value" — descriptor says emit: ${JSON.stringify(descriptor.emit)}; pin one legitimate facet (§3.5)`);
|
|
533
|
+
}
|
|
534
|
+
const decl = new RegExp(`(?:^|[\\n;{])\\s*(?:(?:public|internal|private|fileprivate|open|final|indirect)\\s+)*enum\\s+${escapeRegExp(symbol)}\\b`).exec(text);
|
|
535
|
+
if (!decl) {
|
|
536
|
+
throw new ExtractError(`no swift-enum declaration of "${symbol}" found — wrong pointer, or the anchor is no longer reified`);
|
|
537
|
+
}
|
|
538
|
+
const open = text.indexOf('{', decl.index + decl[0].length);
|
|
539
|
+
if (open === -1) throw new ExtractError(`enum "${symbol}" has no body — nothing to extract`);
|
|
540
|
+
const { inner: body } = swiftBalancedSpan(text, open, '{', '}', symbol);
|
|
541
|
+
|
|
542
|
+
const cases = [];
|
|
543
|
+
let depth = 0;
|
|
544
|
+
let i = 0;
|
|
545
|
+
while (i < body.length) {
|
|
546
|
+
const skipped = skipSwiftComment(body, i, symbol);
|
|
547
|
+
if (skipped !== i) { i = skipped; continue; }
|
|
548
|
+
swiftDirectiveSentinel(body, i, symbol);
|
|
549
|
+
const ch = body[i];
|
|
550
|
+
if (ch === '"') { i = swiftStringValue(body, i, symbol).end; continue; }
|
|
551
|
+
if (ch === '{') { depth += 1; i += 1; continue; }
|
|
552
|
+
if (ch === '}') { depth -= 1; i += 1; continue; }
|
|
553
|
+
if (ch === '`') {
|
|
554
|
+
throw new EnvelopeError(`enum "${symbol}" carries a backtick-escaped identifier — its runtime spelling drops the backticks, so the name could never byte-match its claim (§3.5); out of the swift-enum envelope`);
|
|
555
|
+
}
|
|
556
|
+
if (/[A-Za-z_]/.test(ch)) {
|
|
557
|
+
const word = /^[A-Za-z_][A-Za-z0-9_]*/.exec(body.slice(i))[0];
|
|
558
|
+
i += word.length;
|
|
559
|
+
if (word !== 'case' || depth !== 0) continue; // pattern-match `case`s at depth>0 are not declarations
|
|
560
|
+
// One `case` declaration: comma-separated elements, each an identifier
|
|
561
|
+
// with an optional `= "raw"` string initializer.
|
|
562
|
+
for (;;) {
|
|
563
|
+
i = skipSwiftTrivia(body, i, symbol);
|
|
564
|
+
swiftDirectiveSentinel(body, i, symbol);
|
|
565
|
+
if (body[i] === '`') {
|
|
566
|
+
throw new EnvelopeError(`enum "${symbol}" carries a backtick-escaped case name — its runtime spelling drops the backticks (§3.5); out of the swift-enum envelope`);
|
|
567
|
+
}
|
|
568
|
+
const name = /^[A-Za-z_][A-Za-z0-9_]*/.exec(body.slice(i));
|
|
569
|
+
if (!name) {
|
|
570
|
+
throw new EnvelopeError(`enum "${symbol}": unreadable case name at ${JSON.stringify(body.slice(i, i + 12))}… — outside the swift-enum envelope`);
|
|
571
|
+
}
|
|
572
|
+
i += name[0].length;
|
|
573
|
+
i = skipSwiftTrivia(body, i, symbol);
|
|
574
|
+
if (body[i] === '(' || body[i] === '<') {
|
|
575
|
+
throw new EnvelopeError(`enum "${symbol}" case ${JSON.stringify(name[0])} carries associated values — the value set is a payload type, not a lexically knowable string set; outside the swift-enum envelope (PRD §5.1)`);
|
|
576
|
+
}
|
|
577
|
+
let raw = null;
|
|
578
|
+
if (body[i] === '=') {
|
|
579
|
+
i = skipSwiftTrivia(body, i + 1, symbol);
|
|
580
|
+
swiftDirectiveSentinel(body, i, symbol);
|
|
581
|
+
if (body[i] !== '"') {
|
|
582
|
+
throw new EnvelopeError(`enum "${symbol}" case ${JSON.stringify(name[0])} has a non-string raw value (${JSON.stringify(body.slice(i, i + 12))}…) — not a string set (§3.5); outside the swift-enum envelope`);
|
|
583
|
+
}
|
|
584
|
+
const s = swiftStringValue(body, i, `${symbol}.${name[0]}`);
|
|
585
|
+
raw = s.value;
|
|
586
|
+
i = s.end;
|
|
587
|
+
i = skipSwiftTrivia(body, i, symbol);
|
|
588
|
+
}
|
|
589
|
+
cases.push({ name: name[0], raw });
|
|
590
|
+
if (body[i] === ',') { i += 1; continue; }
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
i += 1;
|
|
596
|
+
}
|
|
597
|
+
if (cases.length === 0) throw new ExtractError(`enum "${symbol}" has no cases — nothing to extract`);
|
|
598
|
+
if (emit === 'raw-value') {
|
|
599
|
+
return cases.map(({ name, raw }) => {
|
|
600
|
+
if (raw === null) {
|
|
601
|
+
throw new EnvelopeError(`enum "${symbol}" case ${JSON.stringify(name)} has no explicit raw value, but the descriptor pins emit: raw-value — Swift derives the implicit raw value from the inheritance clause's semantics, which this lexical grammar refuses to evaluate (§3.5)`);
|
|
602
|
+
}
|
|
603
|
+
return raw;
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
return cases.map(({ name }) => name);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* `swift-const-array` — string elements of a `let`/`var` (usually
|
|
611
|
+
* `static let`) string-array literal (facet: the element strings). Envelope:
|
|
612
|
+
* string literals, commas, and comments inside the brackets. Sentinels:
|
|
613
|
+
* `#if` in the span, string interpolation / escape sequences, non-string
|
|
614
|
+
* members, and concatenation (`+`) either inside the brackets or immediately
|
|
615
|
+
* after the literal — a computed value set is not lexically knowable. A
|
|
616
|
+
* non-array initializer (e.g. `core + regional` with no literal at all, or
|
|
617
|
+
* `all.map { … }`) is an ExtractError: out of this kind's reach entirely.
|
|
618
|
+
*/
|
|
619
|
+
function swiftConstArray(text, descriptor) {
|
|
620
|
+
const symbol = requireSymbol(descriptor, 'swift-const-array');
|
|
621
|
+
const decl = new RegExp(`(?:^|[\\n;{])\\s*(?:(?:public|internal|private|fileprivate|open|static|final|lazy)\\s+)*(?:let|var)\\s+${escapeRegExp(symbol)}\\b`).exec(text);
|
|
622
|
+
if (!decl) {
|
|
623
|
+
throw new ExtractError(`no swift-const-array declaration of "${symbol}" found — wrong pointer, or the anchor is no longer reified`);
|
|
624
|
+
}
|
|
625
|
+
const eq = text.indexOf('=', decl.index + decl[0].length);
|
|
626
|
+
if (eq === -1) throw new ExtractError(`declaration of "${symbol}" carries no initializer — nothing to extract`);
|
|
627
|
+
const at = skipSwiftTrivia(text, eq + 1, symbol);
|
|
628
|
+
if (text[at] !== '[') {
|
|
629
|
+
throw new ExtractError(`"${symbol}" is not initialized with an array literal — a computed or derived value set is out of swift-const-array's reach`);
|
|
630
|
+
}
|
|
631
|
+
const { inner, end } = swiftBalancedSpan(text, at, '[', ']', symbol);
|
|
632
|
+
// `["a"] + more`: the literal is real but not the whole value set.
|
|
633
|
+
const after = skipSwiftTrivia(text, end + 1, symbol);
|
|
634
|
+
if (text[after] === '+') {
|
|
635
|
+
throw new EnvelopeError(`"${symbol}" concatenates the literal with another array ("+") — the full member set is not lexically knowable; extracting the literal members would be a confident wrong parse (PRD §5.1)`);
|
|
636
|
+
}
|
|
637
|
+
const values = [];
|
|
638
|
+
let i = 0;
|
|
639
|
+
while (i < inner.length) {
|
|
640
|
+
const skipped = skipSwiftComment(inner, i, symbol);
|
|
641
|
+
if (skipped !== i) { i = skipped; continue; }
|
|
642
|
+
swiftDirectiveSentinel(inner, i, symbol);
|
|
643
|
+
const ch = inner[i];
|
|
644
|
+
if (/\s/.test(ch)) { i += 1; continue; }
|
|
645
|
+
if (ch === '"') {
|
|
646
|
+
const s = swiftStringValue(inner, i, symbol);
|
|
647
|
+
values.push(s.value);
|
|
648
|
+
i = s.end;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (ch === ',') { i += 1; continue; }
|
|
652
|
+
if (ch === '+') {
|
|
653
|
+
throw new EnvelopeError(`"${symbol}" concatenates arrays ("+") inside the literal — the full member set is not lexically knowable (PRD §5.1)`);
|
|
654
|
+
}
|
|
655
|
+
throw new EnvelopeError(`"${symbol}" carries a non-string-literal member (${JSON.stringify(inner.slice(i, i + 12))}…) — outside the swift-const-array envelope (string literals and commas only)`);
|
|
656
|
+
}
|
|
657
|
+
return values;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ----------------------------------------------------------- the config kinds
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Coerce-refuse (§3.5): js-yaml stringifies every mapping key on load, so a
|
|
664
|
+
* key whose spelling re-resolves to a non-string scalar (`true`, `2027`,
|
|
665
|
+
* `null`, …) cannot be proven to have been a string in the source — quoted
|
|
666
|
+
* `"true"` and bare `true` are indistinguishable after load, and byte-exact
|
|
667
|
+
* equality cannot hold for a coerced key. Such keys are out-of-envelope
|
|
668
|
+
* sentinels, never silently stringified. (js-yaml's default schema follows
|
|
669
|
+
* YAML 1.2 core: `on`/`yes`/`no` load as strings and pass; the classic 1.1
|
|
670
|
+
* coercions — `true`, numerics, `null` — are exactly what this refuses.)
|
|
671
|
+
* A key that only round-trips QUOTED (re-parse throws) must have been quoted
|
|
672
|
+
* in the source, hence a string — accepted.
|
|
673
|
+
*/
|
|
674
|
+
function refuseCoercibleYamlKey(key, kind, where) {
|
|
675
|
+
let reparsed;
|
|
676
|
+
try {
|
|
677
|
+
reparsed = yamlLoad(key);
|
|
678
|
+
} catch {
|
|
679
|
+
return; // only representable quoted — provably a string in the source
|
|
680
|
+
}
|
|
681
|
+
if (reparsed !== key) {
|
|
682
|
+
throw new EnvelopeError(`${kind}: key ${JSON.stringify(key)} ${where} is not provably a string — its spelling resolves to a non-string YAML scalar, so quoted-string vs coerced-scalar spellings are indistinguishable after load and §3.5 byte-exact equality cannot hold; out of this kind's envelope (coerce-refuse)`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Shared YAML body: parse with js-yaml `load` (a data parser — data, never
|
|
688
|
+
* code, the same D-014 rationale as JSON.parse; js-yaml rejects duplicate
|
|
689
|
+
* keys and multi-document streams, both surfacing here as ExtractError),
|
|
690
|
+
* navigate the optional dotted path, and return the target mapping's keys
|
|
691
|
+
* (facet: key strings), coerce-refusing any non-string key.
|
|
692
|
+
*/
|
|
693
|
+
function yamlKeysAt(text, path, kind) {
|
|
694
|
+
let data;
|
|
695
|
+
try {
|
|
696
|
+
data = yamlLoad(text);
|
|
697
|
+
} catch (error) {
|
|
698
|
+
throw new ExtractError(`source is not valid YAML: ${error.message}`);
|
|
699
|
+
}
|
|
700
|
+
let target = data;
|
|
701
|
+
const crumbs = [];
|
|
702
|
+
for (const step of path) {
|
|
703
|
+
crumbs.push(step);
|
|
704
|
+
if (typeof target !== 'object' || target === null || Array.isArray(target) || !Object.hasOwn(target, step)) {
|
|
705
|
+
throw new ExtractError(`path "${crumbs.join('.')}" not found in the YAML document — wrong pointer, or the block moved`);
|
|
706
|
+
}
|
|
707
|
+
target = target[step];
|
|
708
|
+
}
|
|
709
|
+
if (typeof target !== 'object' || target === null || Array.isArray(target)) {
|
|
710
|
+
throw new ExtractError(`${kind} needs a mapping at ${path.length ? `path "${path.join('.')}"` : 'the top level'}, found ${Array.isArray(target) ? 'a sequence' : JSON.stringify(target === null ? null : typeof target)}`);
|
|
711
|
+
}
|
|
712
|
+
const where = path.length ? `at path "${path.join('.')}"` : 'at the top level';
|
|
713
|
+
const keys = Object.keys(target);
|
|
714
|
+
for (const key of keys) refuseCoercibleYamlKey(key, kind, where);
|
|
715
|
+
return keys;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** `yaml-keys` — top-level keys of a YAML document (facet: key strings). */
|
|
719
|
+
function yamlKeys(text) {
|
|
720
|
+
return yamlKeysAt(text, [], 'yaml-keys');
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* `yaml-map-keys` — keys of the mapping at the descriptor's dotted `symbol:`
|
|
725
|
+
* path (e.g. `symbol: flags.canvas`). Facet: key strings.
|
|
726
|
+
*/
|
|
727
|
+
function yamlMapKeys(text, descriptor) {
|
|
728
|
+
const symbol = requireSymbol(descriptor, 'yaml-map-keys');
|
|
729
|
+
return yamlKeysAt(text, symbol.split('.'), 'yaml-map-keys');
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* `.strings` grammar: block and line comments, blank space, and
|
|
734
|
+
* `"key" = "value";` entries — nothing else. Keys are the emitted facet, so
|
|
735
|
+
* an escape sequence in a KEY is a §3.5 sentinel; escapes inside VALUES are
|
|
736
|
+
* fine (values are skipped, not emitted). Anything outside that grammar —
|
|
737
|
+
* a bare token, a pair missing its `=` or `;` — is an EnvelopeError: this
|
|
738
|
+
* format has no recoverable middle ground, and skipping an unreadable line
|
|
739
|
+
* would silently shrink the key set (PRD §5).
|
|
740
|
+
*/
|
|
741
|
+
function dotStringsKeys(text) {
|
|
742
|
+
const context = 'strings-keys';
|
|
743
|
+
const values = [];
|
|
744
|
+
let i = 0;
|
|
745
|
+
while (i < text.length) {
|
|
746
|
+
if (/\s/.test(text[i])) { i += 1; continue; }
|
|
747
|
+
if (text[i] === '/' && text[i + 1] === '/') {
|
|
748
|
+
const nl = text.indexOf('\n', i);
|
|
749
|
+
i = nl === -1 ? text.length : nl + 1;
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (text[i] === '/' && text[i + 1] === '*') {
|
|
753
|
+
const end = text.indexOf('*/', i + 2);
|
|
754
|
+
if (end === -1) throw new ExtractError(`${context}: unterminated block comment`);
|
|
755
|
+
i = end + 2;
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (text[i] !== '"') {
|
|
759
|
+
throw new EnvelopeError(`${context}: unexpected ${JSON.stringify(text.slice(i, i + 12))}… — a .strings file carries only comments and \`"key" = "value";\` entries; anything else is outside the envelope (a skipped line would silently shrink the key set, PRD §5)`);
|
|
760
|
+
}
|
|
761
|
+
// Key string: escapes sentinel — the key is the emitted value (§3.5).
|
|
762
|
+
let j = i + 1;
|
|
763
|
+
while (j < text.length && text[j] !== '"') {
|
|
764
|
+
if (text[j] === '\\') {
|
|
765
|
+
throw new EnvelopeError(`${context}: key ${JSON.stringify(text.slice(i, j + 2))}… carries an escape sequence — its runtime bytes differ from their source spelling, so the key could never byte-match its claim (§3.5)`);
|
|
766
|
+
}
|
|
767
|
+
if (text[j] === '\n') throw new ExtractError(`${context}: unterminated key string`);
|
|
768
|
+
j += 1;
|
|
769
|
+
}
|
|
770
|
+
if (j >= text.length) throw new ExtractError(`${context}: unterminated key string`);
|
|
771
|
+
const key = text.slice(i + 1, j);
|
|
772
|
+
i = j + 1;
|
|
773
|
+
while (i < text.length && /\s/.test(text[i])) i += 1;
|
|
774
|
+
if (text[i] !== '=') {
|
|
775
|
+
throw new EnvelopeError(`${context}: entry ${JSON.stringify(key)} is not followed by "=" — malformed \`"key" = "value";\` pair, outside the envelope`);
|
|
776
|
+
}
|
|
777
|
+
i += 1;
|
|
778
|
+
while (i < text.length && /\s/.test(text[i])) i += 1;
|
|
779
|
+
if (text[i] !== '"') {
|
|
780
|
+
throw new EnvelopeError(`${context}: entry ${JSON.stringify(key)} has no string value after "=" — malformed pair, outside the envelope`);
|
|
781
|
+
}
|
|
782
|
+
// Value string: skipped, not emitted — escaped quotes inside are fine.
|
|
783
|
+
j = i + 1;
|
|
784
|
+
while (j < text.length && text[j] !== '"') j += text[j] === '\\' ? 2 : 1;
|
|
785
|
+
if (j >= text.length) throw new ExtractError(`${context}: unterminated value string for key ${JSON.stringify(key)}`);
|
|
786
|
+
i = j + 1;
|
|
787
|
+
while (i < text.length && /\s/.test(text[i])) i += 1;
|
|
788
|
+
if (text[i] !== ';') {
|
|
789
|
+
throw new EnvelopeError(`${context}: entry ${JSON.stringify(key)} is not terminated by ";" — malformed pair, outside the envelope`);
|
|
790
|
+
}
|
|
791
|
+
i += 1;
|
|
792
|
+
values.push(key);
|
|
793
|
+
}
|
|
794
|
+
if (values.length === 0) throw new ExtractError(`${context}: the file carries no \`"key" = "value";\` entries — nothing to extract`);
|
|
795
|
+
return values;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* `strings-keys` — keys of an Apple localization table, in either format:
|
|
800
|
+
* classic `.strings` (`"key" = "value";` entries) or `.xcstrings` (a JSON
|
|
801
|
+
* catalog whose top-level "strings" object carries the keys). Facet: key
|
|
802
|
+
* strings. DISPATCH IS BY FILE EXTENSION of `descriptor.source` — never
|
|
803
|
+
* content sniffing: the two grammars are disjoint, and guessing one from
|
|
804
|
+
* lookalike bytes is exactly the confident-wrong-parse failure class
|
|
805
|
+
* (PRD §5.1). A source with neither extension is an ExtractError.
|
|
806
|
+
* The .xcstrings side rides jsonKeysAt (JSON.parse — data, never code).
|
|
807
|
+
*/
|
|
808
|
+
function stringsKeys(text, descriptor) {
|
|
809
|
+
const source = typeof descriptor.source === 'string' ? descriptor.source : '';
|
|
810
|
+
if (/\.xcstrings$/i.test(source)) return jsonKeysAt(text, ['strings'], 'strings-keys');
|
|
811
|
+
if (/\.strings$/i.test(source)) return dotStringsKeys(text);
|
|
812
|
+
throw new ExtractError(`strings-keys reads .strings or .xcstrings files, dispatched by the source's file extension — ${JSON.stringify(source)} is neither, and guessing a grammar from content would be a confident wrong parse (PRD §5.1)`);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// -------------------------------------------------------- the directory kind
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* The caller-side half of the directory dispatch seam: produce the
|
|
819
|
+
* deterministic listing a `reads: 'directory'` kind consumes. Sorted by name
|
|
820
|
+
* (byte-wise, the shared engine ordering) so the extraction — and every
|
|
821
|
+
* message derived from it — is independent of filesystem enumeration order.
|
|
822
|
+
* `isDirectory`/`isSymbolicLink` are readdir's lstat-level dirent answers: a
|
|
823
|
+
* symlink reports `isSymbolicLink: true` and `isDirectory: false` whatever
|
|
824
|
+
* it points at, which is exactly what dir-modules' envelope needs to see.
|
|
825
|
+
* Throws the raw fs error (ENOENT/ENOTDIR/EACCES) — the caller maps it to
|
|
826
|
+
* source-missing, same as an unreadable file.
|
|
827
|
+
*/
|
|
828
|
+
export function listDirectory(dirPath) {
|
|
829
|
+
return readdirSync(dirPath, { withFileTypes: true })
|
|
830
|
+
.map((e) => ({ name: e.name, isDirectory: e.isDirectory(), isSymbolicLink: e.isSymbolicLink() }))
|
|
831
|
+
.sort((a, b) => compare(a.name, b.name));
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* dir-modules `pattern:` grammar — a deliberately tiny glob: `*` matches any
|
|
836
|
+
* run of characters (including none) within a NAME; every other character is
|
|
837
|
+
* literal; the pattern must match the WHOLE name. No dependency, no other
|
|
838
|
+
* metacharacters: `?`, `[`, `]`, `{`, `}` are refused loudly (ExtractError),
|
|
839
|
+
* never treated as literals — a pattern the author meant as a richer glob
|
|
840
|
+
* silently matching nothing (or the wrong names) would be a confident wrong
|
|
841
|
+
* parse (PRD §5.1).
|
|
842
|
+
*/
|
|
843
|
+
function patternToRegExp(pattern) {
|
|
844
|
+
const meta = /[?[\]{}]/.exec(pattern);
|
|
845
|
+
if (meta) {
|
|
846
|
+
throw new ExtractError(`dir-modules pattern ${JSON.stringify(pattern)} carries ${JSON.stringify(meta[0])} — the pattern grammar is "*" as the only wildcard (any run of characters), everything else literal, whole-name match; richer glob syntax is refused rather than silently misread (PRD §5.1)`);
|
|
847
|
+
}
|
|
848
|
+
return new RegExp(`^${pattern.split('*').map(escapeRegExp).join('.*')}$`);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* `dir-modules` — directory-listing extraction (PRD §5.1): the value set is
|
|
853
|
+
* NAMES IN A DIRECTORY, for anchors whose members are one file or subfolder
|
|
854
|
+
* each (asset catalogs, per-vertical modules, file-based routing). A directory
|
|
855
|
+
* kind: `{ reads: 'directory', extract(entries, descriptor) }` — see the
|
|
856
|
+
* dispatch-seam note in the module header.
|
|
857
|
+
*
|
|
858
|
+
* Facet (pinned by `pattern:` presence, §3.5):
|
|
859
|
+
* - no `pattern` — the SUBDIRECTORY names (folder-identity modules:
|
|
860
|
+
* src/verticals → editor, prototyping, whiteboard). Plain files in the listing
|
|
861
|
+
* are not part of the facet (a README beside module folders is scenery).
|
|
862
|
+
* - with `pattern` — the FILE names matching the pattern (file-identity
|
|
863
|
+
* modules: routes/*.route.ts → home.route.ts, …). Subdirectories are not
|
|
864
|
+
* part of the facet; non-matching files are excluded BY DECLARATION —
|
|
865
|
+
* the pattern is the descriptor's own envelope, not a guess.
|
|
866
|
+
* Dot-prefixed names (.git, .DS_Store, .build) are hidden-by-convention
|
|
867
|
+
* tooling residue and are never part of either facet.
|
|
868
|
+
*
|
|
869
|
+
* `strip:` (optional) removes a declared suffix from every emitted name
|
|
870
|
+
* (strip `.route.ts`: home.route.ts → home; works on either facet, e.g.
|
|
871
|
+
* stripping `.imageset` from asset-catalog subfolders). A facet name that
|
|
872
|
+
* does not end with the suffix — or that IS the suffix, leaving nothing —
|
|
873
|
+
* hard-errors: emitting it unstripped (or empty) would be a confident wrong
|
|
874
|
+
* parse, never a value.
|
|
875
|
+
*
|
|
876
|
+
* Envelope: a SYMLINK anywhere in the (non-hidden) listing is out-of-envelope
|
|
877
|
+
* — a symlinked module can point outside the surveyed tree, so the listing
|
|
878
|
+
* does not honestly describe what the anchor contains (D-005/D-012).
|
|
879
|
+
*
|
|
880
|
+
* Output is sorted by name (deterministic regardless of filesystem order);
|
|
881
|
+
* an empty facet is an ExtractError — wrong pointer or wrong pattern, never
|
|
882
|
+
* a silently empty set.
|
|
883
|
+
*/
|
|
884
|
+
function dirModules(entries, descriptor) {
|
|
885
|
+
const { pattern, strip } = descriptor;
|
|
886
|
+
if (pattern !== undefined && (typeof pattern !== 'string' || pattern === '')) {
|
|
887
|
+
throw new ExtractError(`dir-modules "pattern:" must be a non-empty string when present — got ${JSON.stringify(pattern)}`);
|
|
888
|
+
}
|
|
889
|
+
if (strip !== undefined && (typeof strip !== 'string' || strip === '')) {
|
|
890
|
+
throw new ExtractError(`dir-modules "strip:" must be a non-empty string when present — got ${JSON.stringify(strip)}`);
|
|
891
|
+
}
|
|
892
|
+
const listed = entries.filter((e) => !e.name.startsWith('.'));
|
|
893
|
+
for (const e of listed) {
|
|
894
|
+
if (e.isSymbolicLink) {
|
|
895
|
+
throw new EnvelopeError(`directory entry ${JSON.stringify(e.name)} is a symlink — it can point outside the surveyed tree, so the listing does not honestly describe what this anchor contains; out of the dir-modules envelope (PRD §5.1)`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
const facet = pattern === undefined
|
|
899
|
+
? listed.filter((e) => e.isDirectory)
|
|
900
|
+
: listed.filter((e) => !e.isDirectory && patternToRegExp(pattern).test(e.name));
|
|
901
|
+
const values = facet.map(({ name }) => {
|
|
902
|
+
if (strip === undefined) return name;
|
|
903
|
+
if (!name.endsWith(strip) || name === strip) {
|
|
904
|
+
throw new ExtractError(`dir-modules cannot strip ${JSON.stringify(strip)} from ${JSON.stringify(name)} — ${name === strip ? 'nothing would remain' : 'the name does not carry the suffix'}; emitting it as-is would be a confident wrong parse (PRD §5.1), so pin the facet with a "pattern:" that excludes it or fix the strip suffix`);
|
|
905
|
+
}
|
|
906
|
+
return name.slice(0, -strip.length);
|
|
907
|
+
});
|
|
908
|
+
if (values.length === 0) {
|
|
909
|
+
throw new ExtractError(`the directory carries no ${pattern === undefined ? 'subdirectories' : `files matching ${JSON.stringify(pattern)}`} — nothing dir-modules can extract (wrong pointer, or the modules moved)`);
|
|
910
|
+
}
|
|
911
|
+
return values.sort(compare);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// ---------------------------------------------------------------- the registry
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Kind name -> recipe. KK-08 registers the TS/JS + JSON kinds (PRD §5.1);
|
|
918
|
+
* KK-09 the Swift + config kinds; KK-10 the dir-modules directory kind (the
|
|
919
|
+
* one `{ reads: 'directory' }` entry — see the dispatch-seam note in the
|
|
920
|
+
* module header); clients author later kinds through the §5.2 pipeline
|
|
921
|
+
* (D-005: only vendored, versioned, test-covered kinds ever run).
|
|
922
|
+
*/
|
|
923
|
+
export const KINDS = Object.freeze({
|
|
924
|
+
/**
|
|
925
|
+
* Newline-delimited value list: one value per line, byte-exact (no
|
|
926
|
+
* trimming beyond the newline), `#` comment lines and blank lines skipped.
|
|
927
|
+
* Envelope: a line opening with `@if` marks conditional inclusion this
|
|
928
|
+
* grammar cannot evaluate — hard error, never a guess. (KK-07's dispatch-
|
|
929
|
+
* proving kind; kept registered for registry/test files that really are
|
|
930
|
+
* newline-delimited.)
|
|
931
|
+
*/
|
|
932
|
+
'test-lines': (text) => {
|
|
933
|
+
const values = [];
|
|
934
|
+
for (const line of text.replace(/\r\n/g, '\n').split('\n')) {
|
|
935
|
+
if (line === '' || line.startsWith('#')) continue;
|
|
936
|
+
if (/^@if\b/.test(line)) {
|
|
937
|
+
throw new EnvelopeError(`out-of-envelope sentinel "${line}" — conditional inclusion is outside this kind's grammar; a confident wrong parse is a false all-clear (PRD §5)`);
|
|
938
|
+
}
|
|
939
|
+
values.push(line);
|
|
940
|
+
}
|
|
941
|
+
return values;
|
|
942
|
+
},
|
|
943
|
+
'ts-const-array': tsConstArray,
|
|
944
|
+
'ts-union': tsUnion,
|
|
945
|
+
'ts-enum': tsEnum,
|
|
946
|
+
'ts-object-keys': tsObjectKeys,
|
|
947
|
+
'json-keys': jsonKeys,
|
|
948
|
+
'json-map-keys': jsonMapKeys,
|
|
949
|
+
'swift-enum': swiftEnum,
|
|
950
|
+
'swift-const-array': swiftConstArray,
|
|
951
|
+
'yaml-keys': yamlKeys,
|
|
952
|
+
'yaml-map-keys': yamlMapKeys,
|
|
953
|
+
'strings-keys': stringsKeys,
|
|
954
|
+
'dir-modules': Object.freeze({ reads: 'directory', extract: dirModules }),
|
|
955
|
+
});
|