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.
Files changed (147) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +5 -0
  3. package/README.md +167 -0
  4. package/cli/.gitkeep +0 -0
  5. package/cli/commands/init-copy.js +90 -0
  6. package/cli/commands/init.js +386 -0
  7. package/cli/init-copy.js +24 -0
  8. package/cli/init.js +24 -0
  9. package/cli/kit.manifest.yaml +256 -0
  10. package/cli/lib/copy-payload.js +334 -0
  11. package/cli/lib/generate-wrappers.js +162 -0
  12. package/package.json +36 -0
  13. package/payload/adapter-fixtures/README.md +53 -0
  14. package/payload/adapter-fixtures/html/EXPECTED.yaml +50 -0
  15. package/payload/adapter-fixtures/html/sample.html +38 -0
  16. package/payload/adapter-fixtures/md/EXPECTED.yaml +65 -0
  17. package/payload/adapter-fixtures/md/sample.md +32 -0
  18. package/payload/adapter-fixtures/pdf/EXPECTED.yaml +45 -0
  19. package/payload/adapter-fixtures/pdf/sample.pdf +0 -0
  20. package/payload/adapter-fixtures/txt/EXPECTED.yaml +31 -0
  21. package/payload/adapter-fixtures/txt/sample.txt +18 -0
  22. package/payload/docs/README.md +102 -0
  23. package/payload/docs/boundaries.md +60 -0
  24. package/payload/docs/ci-wiring.md +109 -0
  25. package/payload/docs/steward-guide.md +238 -0
  26. package/payload/engine/audit.js +34 -0
  27. package/payload/engine/commands/audit.js +289 -0
  28. package/payload/engine/commands/derive.js +334 -0
  29. package/payload/engine/commands/ingest.js +124 -0
  30. package/payload/engine/commands/log-entry.js +85 -0
  31. package/payload/engine/commands/phoenix.js +206 -0
  32. package/payload/engine/commands/preflight.js +530 -0
  33. package/payload/engine/commands/resolve.js +1678 -0
  34. package/payload/engine/commands/survey-map.js +351 -0
  35. package/payload/engine/commands/validate-values.js +315 -0
  36. package/payload/engine/commands/validate.js +1426 -0
  37. package/payload/engine/derive.js +34 -0
  38. package/payload/engine/ingest.js +34 -0
  39. package/payload/engine/lib/anchor-signatures.js +126 -0
  40. package/payload/engine/lib/boot.js +39 -0
  41. package/payload/engine/lib/call-numbers.js +133 -0
  42. package/payload/engine/lib/cli.js +147 -0
  43. package/payload/engine/lib/coverage.js +849 -0
  44. package/payload/engine/lib/decomposition.js +225 -0
  45. package/payload/engine/lib/derived.js +494 -0
  46. package/payload/engine/lib/engine-refusal.js +40 -0
  47. package/payload/engine/lib/exit-codes.js +14 -0
  48. package/payload/engine/lib/extractor-kinds.js +955 -0
  49. package/payload/engine/lib/format-adapters.js +802 -0
  50. package/payload/engine/lib/id-grammars.js +178 -0
  51. package/payload/engine/lib/iso-date.js +55 -0
  52. package/payload/engine/lib/kit-root.js +101 -0
  53. package/payload/engine/lib/load-stores.js +1624 -0
  54. package/payload/engine/lib/log-entry.js +196 -0
  55. package/payload/engine/lib/phoenix.js +628 -0
  56. package/payload/engine/lib/scoring.js +150 -0
  57. package/payload/engine/lib/suppressions.js +172 -0
  58. package/payload/engine/lib/time-verdicts.js +282 -0
  59. package/payload/engine/lib/usage-error.js +14 -0
  60. package/payload/engine/lib/validate-record.js +504 -0
  61. package/payload/engine/log-entry.js +34 -0
  62. package/payload/engine/phoenix.js +39 -0
  63. package/payload/engine/preflight.js +34 -0
  64. package/payload/engine/resolve.js +34 -0
  65. package/payload/engine/survey-map.js +34 -0
  66. package/payload/engine/validate-values.js +34 -0
  67. package/payload/engine/validate.js +34 -0
  68. package/payload/extractor-fixtures/.gitkeep +0 -0
  69. package/payload/extractor-fixtures/README.md +29 -0
  70. package/payload/extractor-fixtures/swift/strings-keys/EXPECTED.yaml +8 -0
  71. package/payload/extractor-fixtures/swift/strings-keys/sample.strings +15 -0
  72. package/payload/extractor-fixtures/swift/swift-const-array/EXPECTED.yaml +7 -0
  73. package/payload/extractor-fixtures/swift/swift-const-array/sample.swift +21 -0
  74. package/payload/extractor-fixtures/swift/swift-enum/EXPECTED.yaml +8 -0
  75. package/payload/extractor-fixtures/swift/swift-enum/sample.swift +30 -0
  76. package/payload/extractor-fixtures/swift/yaml-keys/EXPECTED.yaml +6 -0
  77. package/payload/extractor-fixtures/swift/yaml-keys/sample.yaml +23 -0
  78. package/payload/extractor-fixtures/swift/yaml-map-keys/EXPECTED.yaml +7 -0
  79. package/payload/extractor-fixtures/swift/yaml-map-keys/sample.yaml +15 -0
  80. package/payload/extractor-fixtures/ts/dir-modules/EXPECTED.yaml +12 -0
  81. package/payload/extractor-fixtures/ts/dir-modules/sample-modules/alpha.widget.ts +1 -0
  82. package/payload/extractor-fixtures/ts/dir-modules/sample-modules/beta.widget.ts +1 -0
  83. package/payload/extractor-fixtures/ts/dir-modules/sample-modules/gamma.widget.ts +1 -0
  84. package/payload/extractor-fixtures/ts/dir-modules/sample-modules/helpers/format.ts +3 -0
  85. package/payload/extractor-fixtures/ts/dir-modules/sample-modules/widgets.test.ts +2 -0
  86. package/payload/extractor-fixtures/ts/json-keys/EXPECTED.yaml +7 -0
  87. package/payload/extractor-fixtures/ts/json-keys/sample.json +5 -0
  88. package/payload/extractor-fixtures/ts/json-map-keys/EXPECTED.yaml +7 -0
  89. package/payload/extractor-fixtures/ts/json-map-keys/sample.json +13 -0
  90. package/payload/extractor-fixtures/ts/ts-const-array/EXPECTED.yaml +6 -0
  91. package/payload/extractor-fixtures/ts/ts-const-array/sample.ts +13 -0
  92. package/payload/extractor-fixtures/ts/ts-enum/EXPECTED.yaml +7 -0
  93. package/payload/extractor-fixtures/ts/ts-enum/sample.ts +11 -0
  94. package/payload/extractor-fixtures/ts/ts-object-keys/EXPECTED.yaml +6 -0
  95. package/payload/extractor-fixtures/ts/ts-object-keys/sample.tsx +23 -0
  96. package/payload/extractor-fixtures/ts/ts-union/EXPECTED.yaml +5 -0
  97. package/payload/extractor-fixtures/ts/ts-union/sample.ts +9 -0
  98. package/payload/hooks/pre-commit +37 -0
  99. package/payload/hooks/reverse-lookup +66 -0
  100. package/payload/package.json +3 -0
  101. package/payload/protocol/.gitkeep +0 -0
  102. package/payload/protocol/AGENTS.md +239 -0
  103. package/payload/protocol/derived-layer.md +174 -0
  104. package/payload/protocol/new-kind-pipeline.md +179 -0
  105. package/payload/protocol/registry-warrant.md +162 -0
  106. package/payload/protocol/skills/kb-build.md +303 -0
  107. package/payload/protocol/skills/knowledge-audit.md +183 -0
  108. package/payload/protocol/skills/knowledge-bootstrap.md +229 -0
  109. package/payload/protocol/skills/knowledge-reflect.md +397 -0
  110. package/payload/schemas/catalog.schema.json +32 -0
  111. package/payload/schemas/decision-entry.schema.json +122 -0
  112. package/payload/schemas/finding.schema.json +77 -0
  113. package/payload/schemas/gap.schema.json +52 -0
  114. package/payload/schemas/graduation-categories.schema.json +64 -0
  115. package/payload/schemas/knowledge-leaf.schema.json +194 -0
  116. package/payload/schemas/miss.schema.json +45 -0
  117. package/payload/schemas/ontology-concept.schema.json +115 -0
  118. package/payload/schemas/phoenix-event.schema.json +76 -0
  119. package/payload/schemas/registry.schema.json +57 -0
  120. package/payload/schemas/rules.schema.json +14 -0
  121. package/payload/schemas/survey-scope.schema.json +23 -0
  122. package/payload/templates/decisions/_catalog.yaml +7 -0
  123. package/payload/templates/decisions/_registries/graduation-categories.yaml +42 -0
  124. package/payload/templates/decisions/phoenix-event.yaml +74 -0
  125. package/payload/templates/decisions/reflect-mint-proposal.yaml +100 -0
  126. package/payload/templates/decisions/registry-minting.yaml +58 -0
  127. package/payload/templates/decisions/trust-graduation.yaml +120 -0
  128. package/payload/templates/decisions/trust-revocation.yaml +106 -0
  129. package/payload/templates/knowledge/_catalog.yaml +9 -0
  130. package/payload/templates/knowledge/_registries/anchor.yaml +42 -0
  131. package/payload/templates/knowledge/_registries/authority-tiers.yaml +32 -0
  132. package/payload/templates/knowledge/_registries/domains.yaml +43 -0
  133. package/payload/templates/knowledge/_registries/form.yaml +38 -0
  134. package/payload/templates/knowledge/_registries/jurisdictions.yaml +20 -0
  135. package/payload/templates/knowledge/_registries/operations.yaml +18 -0
  136. package/payload/templates/knowledge/_registries/stage.yaml +53 -0
  137. package/payload/templates/knowledge/_rules.yaml +6 -0
  138. package/payload/templates/new-kind/README.md +107 -0
  139. package/payload/templates/new-kind/descriptor.example.yaml +18 -0
  140. package/payload/templates/new-kind/fixture/EXPECTED.yaml +6 -0
  141. package/payload/templates/new-kind/fixture/demo-anchor.list +2 -0
  142. package/payload/templates/new-kind/fixture/sample.list +7 -0
  143. package/payload/templates/new-kind/parser.example.js +98 -0
  144. package/payload/templates/ontology/_catalog.yaml +6 -0
  145. package/payload/templates/ontology/_rules.yaml +6 -0
  146. package/payload/wrappers/cursor.mdc +15 -0
  147. package/payload/wrappers/pointer.md +10 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Id grammars (UCS-1142) — one module, one grammar per id space.
3
+ *
4
+ * Every store mints ids in its own shape (§3.5): concepts K-NNN, knowledge
5
+ * leaves dotted notation, decisions D-NNN or a provisional D-YYYY-MM-DD-slug.
6
+ * That shape is checked in two places that must never disagree — the shipped
7
+ * JSON Schemas validate record-level ids, and the structural validator's
8
+ * id-shape check validates catalog rows (plain strings there by design, since
9
+ * a catalog row is a pointer, not a record). It used to be spelled five times:
10
+ * four copy-pasted `$defs/notation` blocks plus a regex literal in the
11
+ * validator, whose human-facing hint travelled separately from the pattern it
12
+ * described. A grammar declared five times is five chances to change four.
13
+ *
14
+ * Here the pattern and the hint are ONE entry, and both consumers read it:
15
+ *
16
+ * - validate-record.js injects `pattern` into each schema's `$defs` as the
17
+ * schema documents load, so the engine validates against this module even
18
+ * though the JSON files carry their own copy (see SCHEMA_DEFS below);
19
+ * - commands/validate.js compiles `pattern` for the id-shape check and
20
+ * quotes `hint` in the finding message.
21
+ *
22
+ * The pattern is stored as a STRING, not a RegExp: JSON Schema's `pattern`
23
+ * keyword takes a string, and the validator compiles what it needs. One
24
+ * spelling serves both, so the two can never drift apart.
25
+ *
26
+ * Adding an id space is this module plus its consumers' existing lookups —
27
+ * no schema edit, no second copy (tests/id-grammars.test.js pins that).
28
+ */
29
+
30
+ const ontology = Object.freeze({
31
+ pattern: '^K-[0-9]+$',
32
+ hint: 'K-NNN',
33
+ });
34
+
35
+ /**
36
+ * The LEGACY dotted notation (UCS-1147) — a leaf's optional display label.
37
+ *
38
+ * Still a grammar, because the field is still validated when present: a
39
+ * malformed notation is a defect whether or not anything treats it as
40
+ * identity. What it is no longer is an id space anything RESOLVES through —
41
+ * `leaf-ref` does not accept it, and no leaf is indexed by it. The hint says
42
+ * "legacy" out loud, so a finding quoting it cannot read as an invitation to
43
+ * cite this way.
44
+ */
45
+ const knowledge = Object.freeze({
46
+ pattern: '^[0-9]+(\\.[0-9]+)*$',
47
+ hint: 'legacy dotted notation, e.g. "362.1"',
48
+ });
49
+
50
+ const decisions = Object.freeze({
51
+ pattern: '^D-([0-9]+|[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9-]+)$',
52
+ hint: 'D-NNN or provisional D-YYYY-MM-DD-slug',
53
+ });
54
+
55
+ /**
56
+ * A leaf's accession id (UCS-1144) — opaque, minted at PR time, never reused,
57
+ * never positional. Six digits is the mint width the K-/D- conventions imply
58
+ * read at library scale; it is FIXED, unlike K-NNN, because an accession
59
+ * carries no structure to grow into: L-000001 and L-1 would be two spellings
60
+ * of one identity, and "never reused" cannot survive two spellings.
61
+ *
62
+ * Since UCS-1147 this is a leaf's REQUIRED identity and the only shape a
63
+ * citation of a leaf may take.
64
+ */
65
+ const accessions = Object.freeze({
66
+ pattern: '^L-[0-9]{6}$',
67
+ // Says what to write AND why the field is there, because the reader of this
68
+ // hint is most often an author whose leaf predates the contract: the
69
+ // `missing-required` on a leaf's `id` quotes it, and "L-NNNNNN" alone would
70
+ // tell them the shape of a field without saying that it is now the leaf's
71
+ // identity or that assigning one is the migration.
72
+ hint: 'an accession id of the form L-NNNNNN — every leaf mints one as its identity (UCS-1147)',
73
+ });
74
+
75
+ /**
76
+ * What a finding says to an author who cited a leaf the retired way.
77
+ *
78
+ * Lives beside the grammars because it is the human half of the same fact: the
79
+ * pattern says which spellings are legal, and this says what to do about the
80
+ * one that no longer is. Both travel together into every message that quotes
81
+ * them, for the reason this module exists — a hint that drifted from its
82
+ * pattern would send an author to the wrong edit.
83
+ */
84
+ export const ACCESSION_MIGRATION_HINT =
85
+ 'the leaf\'s accession id (L-NNNNNN); the dotted notation is a legacy display '
86
+ + 'label and no longer resolves as a citation';
87
+
88
+ /**
89
+ * Store → the id grammar its ids obey (§3.5).
90
+ *
91
+ * `pattern` is an anchored JSON-Schema-flavored regex source; `hint` is the
92
+ * human-facing shape quoted back in findings, and travels WITH the pattern so
93
+ * a grammar change cannot leave the prose describing the old one.
94
+ *
95
+ * @type {Readonly<Record<string, { pattern: string, hint: string }>>}
96
+ */
97
+ export const ID_GRAMMARS = Object.freeze({
98
+ ontology,
99
+ knowledge,
100
+ decisions,
101
+ accessions,
102
+ /**
103
+ * What a CITATION of a leaf may look like: an accession, and nothing else
104
+ * (UCS-1147's contract phase).
105
+ *
106
+ * It was a UNION of accession and notation while both spellings were legal
107
+ * (UCS-1144's expand phase), and it narrowed here exactly as that ticket
108
+ * predicted — the migrate batches (UCS-1145, UCS-1146) rewrote every
109
+ * notation-form citation first, so narrowing breaks nothing that was left.
110
+ *
111
+ * It stays a named entry rather than collapsing into `accessions` at every
112
+ * call site, and that is deliberate: `accessions` says what a leaf MINTS and
113
+ * `leaf-ref` says what a record may CITE. Those are two claims that happen to
114
+ * coincide today, and consumers read them by name for the same reason they
115
+ * always did — the day they diverge again is a change to this table, not a
116
+ * sweep through the surfaces that judge citations. The `hint` differs for
117
+ * that reason too: a citation finding names the migration, a minting finding
118
+ * names the shape.
119
+ */
120
+ 'leaf-ref': Object.freeze({
121
+ pattern: accessions.pattern,
122
+ hint: ACCESSION_MIGRATION_HINT,
123
+ }),
124
+ });
125
+
126
+ /**
127
+ * Which `$defs` name in the shipped schemas each id space owns.
128
+ *
129
+ * The JSON files keep their own `$defs` copies so they stay self-contained for
130
+ * external tools (a cross-file `$ref` is outside the keyword subset the engine
131
+ * interprets, and would break every consumer that reads one schema alone).
132
+ * This map is how the copies stop being a second source of truth: the loader
133
+ * OVERWRITES each named def's pattern with the one above, so a JSON file that
134
+ * drifts is corrected at load rather than silently believed.
135
+ *
136
+ * Only spaces whose grammar appears in a schema `$defs` are listed —
137
+ * ontology/decisions ids reach the schemas through conceptRef/decisionRef and
138
+ * are not part of the leaf-notation seam this ticket settles.
139
+ *
140
+ * The three leaf entries stay three defs after UCS-1147 narrowed citations to
141
+ * accessions, and the reason is what they each say rather than what they each
142
+ * match. `accession` is a leaf's identity — required, and what `leafRef` cites.
143
+ * `notation` is the optional LEGACY display label, still dotted-only and still
144
+ * validated when present, because a malformed label is a defect even though
145
+ * nothing resolves through it. `leafRef` and `accession` carry the same pattern
146
+ * today; collapsing them would lose the distinction between what a leaf mints
147
+ * and what a record may cite, which is exactly the distinction that let the
148
+ * expand phase widen citations without ever widening leaf notation itself.
149
+ *
150
+ * @type {Readonly<Record<string, string>>}
151
+ */
152
+ export const SCHEMA_DEFS = Object.freeze({
153
+ knowledge: 'notation',
154
+ 'leaf-ref': 'leafRef',
155
+ accessions: 'accession',
156
+ });
157
+
158
+ /**
159
+ * The compiled matcher for an id space. Anchored by construction (the patterns
160
+ * above carry their own ^/$), cached per space so the hot id-shape loop does
161
+ * not recompile.
162
+ *
163
+ * @param {string} space one of the keys of ID_GRAMMARS
164
+ * @returns {RegExp} the compiled grammar
165
+ */
166
+ const compiled = new Map();
167
+ export function idPattern(space) {
168
+ let re = compiled.get(space);
169
+ if (!re) {
170
+ const grammar = ID_GRAMMARS[space];
171
+ if (!grammar) {
172
+ throw new TypeError(`unknown id space "${space}" (expected one of: ${Object.keys(ID_GRAMMARS).join(', ')})`);
173
+ }
174
+ re = new RegExp(grammar.pattern);
175
+ compiled.set(space, re);
176
+ }
177
+ return re;
178
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * What "an ISO date" means, once (UCS-957).
3
+ *
4
+ * The rule was stated four times — in `lib/log-entry.js`, `lib/suppressions.js`,
5
+ * `commands/preflight.js` and `commands/audit.js` — and the copies disagreed.
6
+ * Three checked the SHAPE. Only preflight also asked the calendar.
7
+ *
8
+ * `Date.parse('2026-02-30')` does not fail. It rolls forward to March 2nd. So
9
+ * `audit --today 2026-02-30` exited 0 and reported staleness measured from a
10
+ * day that does not exist, two days later than the caller named, while
11
+ * preflight refused the same string outright.
12
+ *
13
+ * That defeats the reason injectable dates exist. The engine never reads the
14
+ * wall clock (D-012, PRD §5) so that its answers are reproducible from their
15
+ * inputs — and a date silently replaced by a different date is not the input
16
+ * anybody gave. Worse for `log-entry`, whose `--date` is stamped into fragment
17
+ * filenames and status transitions: a rolled-forward date lands in a permanent
18
+ * audit trail.
19
+ *
20
+ * One definition. The strict one.
21
+ */
22
+
23
+ /** The shape: exactly four digits, two, two. Nothing else, no time, no zone. */
24
+ export const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
25
+
26
+ /**
27
+ * True when `value` is a YYYY-MM-DD string naming a day that actually exists.
28
+ *
29
+ * The round-trip is the whole check: `new Date('2026-02-30T00:00:00Z')` is a
30
+ * valid Date object (March 2nd), so `Number.isNaN` alone never catches it.
31
+ * Formatting it back and comparing to the input does.
32
+ *
33
+ * @param {unknown} value
34
+ * @returns {boolean}
35
+ */
36
+ export function isCalendarDate(value) {
37
+ if (typeof value !== 'string' || !ISO_DATE.test(value)) return false;
38
+ const roundTrip = new Date(`${value}T00:00:00Z`);
39
+ if (Number.isNaN(roundTrip.getTime())) return false;
40
+ return roundTrip.toISOString().slice(0, 10) === value;
41
+ }
42
+
43
+ /**
44
+ * Whole days from ISO date `from` to ISO date `to`.
45
+ *
46
+ * Both must already be calendar dates — callers validate at their edge, where
47
+ * they can say which flag was wrong. UTC midnight throughout, so no timezone
48
+ * and no daylight-saving hour can move the answer.
49
+ *
50
+ * @param {string} from
51
+ * @param {string} to
52
+ * @returns {number}
53
+ */
54
+ export const daysBetween = (from, to) =>
55
+ Math.floor((Date.parse(`${to}T00:00:00Z`) - Date.parse(`${from}T00:00:00Z`)) / 86_400_000);
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Where the Kit lives under a repo root — the one authority (UCS-934).
3
+ *
4
+ * Every repo-root-taking surface asks this module two questions, and there is
5
+ * exactly one answer to each: which directory holds the stores, and which
6
+ * root-relative paths are KIT ZONE (kit-vendored data the reverse audit must
7
+ * never propose concepts for — the map is never told to map itself).
8
+ *
9
+ * This used to be answered twice, with opposite tie-breaks: the loader
10
+ * preferred the nested seeded dir, the reverse audit preferred the repo root.
11
+ * A repo carrying both read as two different Stores depending on which surface
12
+ * asked, which voids the single-health-model guarantee — the audit could
13
+ * propose Concepts against one Store while preflight verdicted another.
14
+ *
15
+ * THERE IS NO TIE-BREAK, because the answer is not knowable. A repo carrying
16
+ * BOTH a seeded kit dir and root-level stores is ambiguous in a way no rule
17
+ * decides correctly:
18
+ *
19
+ * - a client with a product `ontology/` who seeds a kit wants the NESTED one
20
+ * - a client who moved their stores to the root, leaving a stale seeded dir
21
+ * behind, wants the ROOT one
22
+ *
23
+ * Both repos look identical from here. Picking either silently reads one Store
24
+ * and ignores the other — a confident wrong answer, which is the failure class
25
+ * this engine exists to prevent. So an ambiguous layout REFUSES: every surface
26
+ * fails identically (exit 2) and names both candidates, rather than four
27
+ * surfaces agreeing on one Store while the audit quietly reads the other.
28
+ *
29
+ * Unambiguous layouts resolve without ceremony: a seeded dir alone is the Kit
30
+ * (the D-016 default `init` creates, and where the generated platform wrappers
31
+ * point every agent); otherwise the repo root is, which is how the kit's own
32
+ * repo eats its own cooking and how every engine test fixture is laid out.
33
+ * Stores absent entirely is not an error: the loader reports missing-store
34
+ * warnings and the audit proposes every anchor.
35
+ */
36
+ import { statSync } from 'node:fs';
37
+ import { join } from 'node:path';
38
+ import { EngineRefusal } from './engine-refusal.js';
39
+
40
+ /** The §9.1/D-016 seeded kit directory name. Renames are a later seam. */
41
+ export const KIT_DIR_DEFAULT = 'unknown-knowledge';
42
+
43
+ /** The human-confirmed survey boundary (§6), written at the kit root. */
44
+ export const SCOPE_FILE = 'survey-scope.yaml';
45
+
46
+ /** Client-zone suppression file (KK-27/D-013) — kit zone, at the kit root. */
47
+ export const SUPPRESSIONS_FILE = 'suppressions.yaml';
48
+
49
+ /** The three governed stores plus the fragment logs (§3, D-010). */
50
+ const STORE_DIRS = ['ontology', 'knowledge', 'decisions', 'logs'];
51
+
52
+ /**
53
+ * Kit zone when the stores live at the scan root itself. The seeded dir name
54
+ * stays listed as belt-and-braces: a seeded DIRECTORY here would now refuse as
55
+ * ambiguous before the zone is ever consulted, but a stray file by that name
56
+ * is still kit-shaped and never product surface to propose concepts for.
57
+ */
58
+ const KIT_ZONE_AT_ROOT = [...STORE_DIRS, SCOPE_FILE, SUPPRESSIONS_FILE, KIT_DIR_DEFAULT];
59
+
60
+ const isDir = (path) => !!statSync(path, { throwIfNoEntry: false })?.isDirectory();
61
+
62
+ /**
63
+ * The repo root is itself a store root when it carries the artifact-owned or
64
+ * world-owned stores. `decisions/` alone does not count: the kit's own repo
65
+ * keeps a decisions store at its root without being a seeded kit.
66
+ */
67
+ const looksLikeStoreRoot = (root) => isDir(join(root, 'ontology')) || isDir(join(root, 'knowledge'));
68
+
69
+ /** A repo whose Kit cannot be identified — never resolved by guessing. */
70
+ export class AmbiguousKitLayout extends EngineRefusal {
71
+ // Without this, `err.name` reads "Error" and a logged refusal hides which
72
+ // failure it was — the exit-2 message should identify itself.
73
+ name = 'AmbiguousKitLayout';
74
+ }
75
+
76
+ /**
77
+ * Locate the Kit under a repo root.
78
+ *
79
+ * @param {string} root the repo root (`--root`), never the store dir
80
+ * @returns {{ kitRoot: string, kitPrefixes: string[] }} the directory holding
81
+ * the stores, and the root-relative paths that are kit zone
82
+ * @throws {AmbiguousKitLayout} when a seeded kit dir and root-level stores
83
+ * both exist, so no surface can know which Store is authoritative
84
+ */
85
+ export function locateKit(root) {
86
+ const nested = join(root, KIT_DIR_DEFAULT);
87
+ if (isDir(nested)) {
88
+ if (looksLikeStoreRoot(root)) {
89
+ throw new AmbiguousKitLayout(
90
+ `two candidate kit roots under ${JSON.stringify(root)}: the seeded ${KIT_DIR_DEFAULT}/ and stores at the root itself. `
91
+ + 'Which one is authoritative is not knowable from here, and guessing would let the reverse audit read a different '
92
+ + 'store than the validators (PRD §4, single health model). Point --root at the intended kit root, or remove the stale one.',
93
+ );
94
+ }
95
+ return { kitRoot: nested, kitPrefixes: [KIT_DIR_DEFAULT] };
96
+ }
97
+ return { kitRoot: root, kitPrefixes: KIT_ZONE_AT_ROOT };
98
+ }
99
+
100
+ /** The store root alone, for surfaces that never scan for product anchors. */
101
+ export const locateKitRoot = (root) => locateKit(root).kitRoot;