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,256 @@
1
+ # cli/kit.manifest.yaml — the payload manifest (KK-17, PRD §6/§9.2, D-007).
2
+ #
3
+ # The EXPLICIT allowlist of everything `npx unknown-knowledge init` seeds into
4
+ # a client repo. Copy is manifest-driven ONLY: a file this manifest does not
5
+ # name is never copied — nothing ships by omission (D-007). The copy engine
6
+ # (cli/lib/copy-payload.js) enforces, by construction, that every `from` path
7
+ # resolves INSIDE payload/ (the allowlist root, §9.2) and never at the kit's
8
+ # acceptance fixtures/ or tests/ — an entry that tries is refused, not
9
+ # trusted.
10
+ #
11
+ # Paths: `from` is relative to payload/; `to` is relative to the seeded root
12
+ # (default unknown-knowledge/, caller-named, never dotted — §6/§9.1).
13
+ # Directory entries vendor every file beneath them, sorted — payload/ is
14
+ # "EXACTLY what init vendors" (§9.2), so a directory inside it is still the
15
+ # allowlist speaking (kit CI pins payload/ ⊆ manifest expansion, so nothing
16
+ # under payload/ can rot unshipped either).
17
+ #
18
+ # The seeded target gets its own generated kit.manifest.yaml (§9.1): manifest
19
+ # echo (full seeded file list), kit version stamp, zone map. Its presence is
20
+ # the §6 seed marker — init REFUSES any existing root, even a partial seed.
21
+ schema-version: 1
22
+
23
+ defaults:
24
+ root: unknown-knowledge # default seeded root dir — visible, never dotted (§6)
25
+
26
+ # ------------------------------------------------------------- unconditional
27
+ # Seeded for every stack selection, including config-only (no stacks).
28
+ unconditional:
29
+ # ESM marker for the seeded engine. The engine is ESM (D-002, zero-build);
30
+ # without this Node re-parses every engine file to guess its module type and
31
+ # prints MODULE_TYPELESS_PACKAGE_JSON on stderr — the same stream the
32
+ # protocol tells agents to read for engine failures, so the noise is worse
33
+ # than cosmetic. It declares a module type and nothing else: dependency
34
+ # resolution still walks up to the client's own node_modules (js-yaml, §9.1).
35
+ module-type:
36
+ - { from: package.json, to: package.json }
37
+ schemas:
38
+ - { from: schemas, to: schemas }
39
+ engine:
40
+ - { from: engine, to: engine }
41
+ protocol:
42
+ # incl. AGENTS.md — the navigation contract + runtime loop (§7).
43
+ - { from: protocol/AGENTS.md, to: protocol/AGENTS.md }
44
+ - { from: protocol/new-kind-pipeline.md, to: protocol/new-kind-pipeline.md }
45
+ # Warrant-minting and suppression conduct for the governed vocabulary
46
+ # registries (UCS-1148) — the rules a steward follows before editing one.
47
+ - { from: protocol/registry-warrant.md, to: protocol/registry-warrant.md }
48
+ # The derived layer (UCS-1158) — what `engine/derive.js` generates, why
49
+ # deleting it loses nothing, and the two rules a reader must not break:
50
+ # never cite a synthesized call number, and never hand-edit a derived file.
51
+ # Also the declared position of embedding recall — a slot and a gate, with
52
+ # the implementation deliberately absent.
53
+ - { from: protocol/derived-layer.md, to: protocol/derived-layer.md }
54
+ # Protocol skills (D-019 names) — the phase-2 procedures agents run.
55
+ - { from: protocol/skills/knowledge-bootstrap.md, to: protocol/skills/knowledge-bootstrap.md }
56
+ - { from: protocol/skills/knowledge-reflect.md, to: protocol/skills/knowledge-reflect.md }
57
+ - { from: protocol/skills/kb-build.md, to: protocol/skills/kb-build.md }
58
+ - { from: protocol/skills/knowledge-audit.md, to: protocol/skills/knowledge-audit.md }
59
+ hooks:
60
+ # Git hooks that enforce the protocol mechanically (UCS-1157): blocking
61
+ # validation before a commit exists, and automatic reverse lookup over
62
+ # the staged diff. They are THIN WRAPPERS — each invokes one engine
63
+ # command and exits with its code, unchanged — so the tested surface is
64
+ # the wrapped command, and the wiring is reviewed the way the per-IDE
65
+ # wrappers are.
66
+ #
67
+ # They SEED but do not INSTALL. init never writes .git/ (the client's own
68
+ # config is the client's), so these land at <root>/hooks/ and the client
69
+ # symlinks or points core.hooksPath at them. That is the same boundary
70
+ # CI wiring keeps (D-006): the kit ships the gate, the client hangs it.
71
+ #
72
+ # The copy engine seeds BYTES, not modes, so a seeded hook arrives
73
+ # non-executable and git will not run it until the client chmods it. That
74
+ # is documented at the wiring step (README) rather than fixed here: a
75
+ # manifest that carried per-entry permissions would be a second thing to
76
+ # keep true, for one bit the client sets in the same breath as the symlink.
77
+ - { from: hooks/pre-commit, to: hooks/pre-commit }
78
+ - { from: hooks/reverse-lookup, to: hooks/reverse-lookup }
79
+ templates:
80
+ # The §5.2 new-kind authoring template — clients extend the extractor
81
+ # library themselves (D-001/D-009), so the template always ships.
82
+ - { from: templates/new-kind, to: templates/new-kind }
83
+ # The Decisions-entry template a steward copies when minting or suppressing
84
+ # a registry value (UCS-1148): every registry edit is a governed act with a
85
+ # written rationale, and this is the shape of that rationale.
86
+ - { from: templates/decisions/registry-minting.yaml, to: templates/decisions/registry-minting.yaml }
87
+ # The Decisions-entry template reflect copies when a corroborated cluster of
88
+ # residue/candidate findings warrants minting a term, alias, operation, or
89
+ # domain class (UCS-1160): every mint is a governed act carrying BOTH its
90
+ # literary warrant and the three-fragment evidence that raised it.
91
+ - { from: templates/decisions/reflect-mint-proposal.yaml, to: templates/decisions/reflect-mint-proposal.yaml }
92
+ # The Decisions-entry template a steward copies to record a phoenix event
93
+ # (UCS-1154): a bulk re-taxonomy is a governed act, and this is the shape of
94
+ # the rationale that sanctions the edition bump it carries.
95
+ - { from: templates/decisions/phoenix-event.yaml, to: templates/decisions/phoenix-event.yaml }
96
+ # The Decisions-entry templates a steward copies to move the trust boundary
97
+ # (UCS-1155): graduating a change category from full inspection to sampling,
98
+ # and revoking that graduation. Both ship, and the pair is the point —
99
+ # revocation must be no harder to reach for than graduation, or the boundary
100
+ # would only ever move one way.
101
+ - { from: templates/decisions/trust-graduation.yaml, to: templates/decisions/trust-graduation.yaml }
102
+ - { from: templates/decisions/trust-revocation.yaml, to: templates/decisions/trust-revocation.yaml }
103
+ # Stack-agnostic fixture authoring guide: ships even with no stack
104
+ # selected — a config-only client authoring its first pack starts here.
105
+ - { from: extractor-fixtures/README.md, to: engine/tests/fixtures/README.md }
106
+ # Format-adapter fixture pairs (UCS-1153). These ship UNCONDITIONALLY,
107
+ # where extractor fixtures ship per selected stack (D-009). The difference
108
+ # is real, not an oversight: an extractor kind is stack-specific (a Swift
109
+ # client has no use for ts-union), but a FORMAT is not — any client on any
110
+ # stack can be handed a PDF or a markdown spec. And the pairs are the
111
+ # authoring template for client-drafted adapters (D-005), so a client whose
112
+ # format is unsupported needs them present at the moment of the refusal,
113
+ # which is exactly when they have no vendor to return to (D-001).
114
+ - { from: adapter-fixtures, to: engine/tests/adapter-fixtures }
115
+ store-scaffolding:
116
+ # Empty seeds (§9.1): catalogs/rules the bootstrap skill fills. Decisions
117
+ # has no _rules.yaml by design (§9.1).
118
+ - { from: templates/ontology/_catalog.yaml, to: ontology/_catalog.yaml }
119
+ - { from: templates/ontology/_rules.yaml, to: ontology/_rules.yaml }
120
+ - { from: templates/knowledge/_catalog.yaml, to: knowledge/_catalog.yaml }
121
+ - { from: templates/knowledge/_rules.yaml, to: knowledge/_rules.yaml }
122
+ - { from: templates/decisions/_catalog.yaml, to: decisions/_catalog.yaml }
123
+ # Governed vocabulary registries (UCS-1148, extended by UCS-1149). All
124
+ # seeded EMPTY, like the catalogs: the bootstrap interview mints the first
125
+ # values from material that already exists, because a value minted ahead of
126
+ # its material is speculative shelving (protocol/registry-warrant.md). They
127
+ # ship as store scaffolding rather than as templates-to-copy because a
128
+ # registry is a LIVE governed file — the client edits this one, it is not a
129
+ # pattern.
130
+ #
131
+ # anchor/ and stage/ are empty for a SECOND reason worth stating: their
132
+ # vocabularies are fixed by the kit (D-003's anchors; the stages the shared
133
+ # pre-promotion predicate reads), so their headers name the exact values to
134
+ # mint. They still ship valueless because every value must cite a Decisions
135
+ # entry that resolves, and a seeded repo's decisions store is empty by
136
+ # design (D-001) — seeded values would hand every client a broken ref.
137
+ - { from: templates/knowledge/_registries/domains.yaml, to: knowledge/_registries/domains.yaml }
138
+ - { from: templates/knowledge/_registries/operations.yaml, to: knowledge/_registries/operations.yaml }
139
+ - { from: templates/knowledge/_registries/jurisdictions.yaml, to: knowledge/_registries/jurisdictions.yaml }
140
+ - { from: templates/knowledge/_registries/authority-tiers.yaml, to: knowledge/_registries/authority-tiers.yaml }
141
+ - { from: templates/knowledge/_registries/form.yaml, to: knowledge/_registries/form.yaml }
142
+ - { from: templates/knowledge/_registries/anchor.yaml, to: knowledge/_registries/anchor.yaml }
143
+ - { from: templates/knowledge/_registries/stage.yaml, to: knowledge/_registries/stage.yaml }
144
+ # The trust graduation category table (UCS-1155). Filed under decisions/
145
+ # because graduation governs the change PROCESS, not the knowledge — the
146
+ # truth anchor is the team (D-003). Seeded empty for the usual reason (every
147
+ # row cites a decision that must resolve) plus one of its own: whether a
148
+ # class of change is mechanical enough to sample is a judgment about THIS
149
+ # project's material, and shipping it pre-made would be the kit deciding how
150
+ # much someone else's work gets read.
151
+ - { from: templates/decisions/_registries/graduation-categories.yaml, to: decisions/_registries/graduation-categories.yaml }
152
+ docs:
153
+ # Client-facing docs (KK-24). The seeded README lands at the kit-dir
154
+ # root; the guides land under docs/.
155
+ - { from: docs/README.md, to: README.md }
156
+ - { from: docs/ci-wiring.md, to: docs/ci-wiring.md }
157
+ - { from: docs/steward-guide.md, to: docs/steward-guide.md }
158
+ - { from: docs/boundaries.md, to: docs/boundaries.md }
159
+
160
+ # ------------------------------------------------------- stack-conditional
161
+ # Extractor fixture packs ship ONLY for the stacks selected at init (D-009);
162
+ # later stacks are client-authored from the shipped template, warned at init.
163
+ # Target path per §9.1: engine/tests/fixtures/<stack>/.
164
+ stacks:
165
+ ts:
166
+ - { from: extractor-fixtures/ts, to: engine/tests/fixtures/ts }
167
+ swift:
168
+ - { from: extractor-fixtures/swift, to: engine/tests/fixtures/swift }
169
+
170
+ # ---------------------------------------------------------------- platforms
171
+ # The agent-platform wrapper REGISTRY (KK-18, PRD §6): data, extensible —
172
+ # adding a platform later = adding an entry here (+ a template if the
173
+ # generic pointer doesn't fit). Wrappers are GENERATED per platform
174
+ # selection by cli/lib/generate-wrappers.js, not copied: `template` is
175
+ # relative to payload/ (same D-007 guards as `from` paths — refused at
176
+ # load if it escapes), `target` is relative to the CLIENT REPO ROOT (the
177
+ # one wrapper exception to the seeded-root scope: conventional paths are
178
+ # the platforms' own, dotted dirs included — the §6 never-dotted rule
179
+ # governs the seeded root only).
180
+ #
181
+ # `mode` is the §6 collision policy:
182
+ # shared — an ecosystem-shared instruction file (root AGENTS.md,
183
+ # .github/copilot-instructions.md): appended between
184
+ # sentinel markers, never overwritten; an existing sentinel
185
+ # block is replaced within the sentinels (idempotent);
186
+ # malformed sentinels skip-and-report.
187
+ # dedicated — a single-tool file: created if absent; an existing file
188
+ # is skip-and-report, never clobbered.
189
+ #
190
+ # Path conventions (verified against current tool conventions, 2026-07):
191
+ # Claude Code → root CLAUDE.md (the simplest durable convention; .claude/
192
+ # holds settings/skills, not the primary instructions pointer). Treated
193
+ # as DEDICATED: it is the user's own tool file, not an ecosystem-shared
194
+ # one — an existing CLAUDE.md is theirs to edit.
195
+ # Cursor → .cursor/rules/unknown-knowledge.mdc (modern project-rules path;
196
+ # legacy .cursorrules is deprecated). Own template: .mdc frontmatter.
197
+ # Copilot → .github/copilot-instructions.md (SHARED per §6).
198
+ # Codex → root AGENTS.md (SHARED per §6 — the wrapper IS a sentinel
199
+ # section in the ecosystem's shared file).
200
+ # Gemini CLI → root GEMINI.md (its default context file). Dedicated.
201
+ platforms:
202
+ claude-code:
203
+ name: Claude Code
204
+ template: wrappers/pointer.md
205
+ target: CLAUDE.md
206
+ mode: dedicated
207
+ codex:
208
+ name: Codex
209
+ template: wrappers/pointer.md
210
+ target: AGENTS.md
211
+ mode: shared
212
+ copilot:
213
+ name: GitHub Copilot
214
+ template: wrappers/pointer.md
215
+ target: .github/copilot-instructions.md
216
+ mode: shared
217
+ cursor:
218
+ name: Cursor
219
+ template: wrappers/cursor.mdc
220
+ target: .cursor/rules/unknown-knowledge.mdc
221
+ mode: dedicated
222
+ gemini:
223
+ name: Gemini CLI
224
+ template: wrappers/pointer.md
225
+ target: GEMINI.md
226
+ mode: dedicated
227
+
228
+ # ---------------------------------------------------------------- create
229
+ # Empty directories the engine creates with a .gitkeep — §6: init "ships
230
+ # .gitkeep in empty log dirs" (a gitignored/absent findings log kills the
231
+ # improvement loop silently). Fragment stores: one file per entry (D-010).
232
+ create:
233
+ - decisions/entries
234
+ - logs/findings
235
+ - logs/misses
236
+ - logs/gaps
237
+
238
+ # -------------------------------------------------------------- root-files
239
+ # Kit-repo ROOT files (the one sanctioned exception to the payload/ boundary,
240
+ # limited to this exact basename allowlist — the engine refuses anything
241
+ # else). Required-at-publish: KK-28 adds LICENSE/NOTICE ("LICENSE/NOTICE in
242
+ # payload manifest"); until they exist the engine treats them as required
243
+ # only when present, so the two PRs compose. Kit CI must gate publish on
244
+ # their presence (KK-28).
245
+ root-files:
246
+ - LICENSE
247
+ - NOTICE
248
+
249
+ # ---------------------------------------------------------------- zone map
250
+ # Echoed into the seeded kit.manifest.yaml (§9.1) — which seeded paths are
251
+ # kit-vendored code vs. client data. Everything is client-OWNED after seed
252
+ # (D-001, seeded-once); the map tells an uninstall/audit what came from the
253
+ # kit vs. what the client's own loop produced.
254
+ zones:
255
+ seeded: [engine, protocol, schemas, templates, kit.manifest.yaml]
256
+ client: [ontology, knowledge, decisions, logs, survey-scope.yaml]
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Payload copy engine (KK-17, PRD §6 Phase 1 / §9.1-§9.2, D-007/D-009) — the
3
+ * manifest-driven library `npx unknown-knowledge init` (KK-19) drives. This
4
+ * is the ENGINE only: prompting UX is KK-19's; callers pass the answers
5
+ * (root name, stacks) as parameters.
6
+ *
7
+ * Copy is manifest-driven ONLY (cli/kit.manifest.yaml): a file the manifest
8
+ * does not name is never copied. Leakage of the kit's acceptance fixtures/
9
+ * and tests/ is impossible BY CONSTRUCTION, not by author discipline:
10
+ *
11
+ * - every `from` must be relative, `..`-free, and resolve INSIDE payload/
12
+ * (the §9.2 allowlist root); the loader refuses the manifest otherwise;
13
+ * - even a hypothetically-contained path that lands under the kit's
14
+ * fixtures/ or tests/ is refused by name — the engine distrusts
15
+ * manifest authors (D-007: nothing ships by omission);
16
+ * - the ONE exception is the `root-files` section, limited to an exact
17
+ * basename allowlist (LICENSE, NOTICE — required-at-publish, KK-28);
18
+ * until those files land they are required only when present, so the
19
+ * parallel PRs compose.
20
+ *
21
+ * Seeding contract (§6):
22
+ * - root dir defaults to `unknown-knowledge/`, caller-named, NEVER dotted
23
+ * (a dot-root hides the knowledge base — refused);
24
+ * - an existing target root — even a partial seed — REFUSES with guidance;
25
+ * re-init/upgrade is deferred (§11.1);
26
+ * - the seeded root gets a generated kit.manifest.yaml (§9.1): kit version
27
+ * stamp (package version — semver semantics are KK-28's), selected
28
+ * stacks, zone map, and the full sorted seeded-file list (the manifest
29
+ * echo doubles as the §6 seed marker).
30
+ *
31
+ * Deterministic by construction: sorted expansion, byte-exact copies, no
32
+ * wall-clock reads — identical inputs seed byte-identical trees (A1).
33
+ */
34
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
35
+ import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
36
+ import { load, YAMLException } from 'js-yaml';
37
+ import { EngineRefusal } from '../../payload/engine/lib/engine-refusal.js';
38
+
39
+ export const DEFAULT_ROOT = 'unknown-knowledge';
40
+ export const MANIFEST_FILE = join('cli', 'kit.manifest.yaml');
41
+ export const SEEDED_MANIFEST = 'kit.manifest.yaml';
42
+ /** The one sanctioned exception to the payload/ boundary (KK-28 files). */
43
+ export const ROOT_FILE_ALLOWLIST = Object.freeze(['LICENSE', 'NOTICE']);
44
+
45
+ /** Refusals (existing seed, bad root name) — expected conditions, not bugs. */
46
+ export class SeedRefusal extends EngineRefusal {
47
+ constructor(message) {
48
+ super(message);
49
+ this.name = 'SeedRefusal';
50
+ this.refused = true;
51
+ }
52
+ }
53
+
54
+ // --------------------------------------------------------------- validation
55
+
56
+ /** A `from`/`to` path must be relative, non-empty, and free of `..`/`.` games. */
57
+ function assertSanePath(value, field, entryDesc) {
58
+ if (typeof value !== 'string' || value.length === 0) {
59
+ throw new Error(`manifest entry ${entryDesc}: ${field} must be a non-empty string`);
60
+ }
61
+ if (isAbsolute(value) || value.includes('\\')) {
62
+ throw new Error(`manifest entry ${entryDesc}: ${field} must be a relative /-separated path, got ${JSON.stringify(value)}`);
63
+ }
64
+ const segments = value.split('/');
65
+ if (segments.some((s) => s === '' || s === '.' || s === '..')) {
66
+ throw new Error(`manifest entry ${entryDesc}: ${field} may not contain empty, "." or ".." segments (got ${JSON.stringify(value)})`);
67
+ }
68
+ }
69
+
70
+ /** True when `path` is `parent` or lives beneath it (both pre-resolved). */
71
+ function within(path, parent) {
72
+ return path === parent || path.startsWith(parent + sep);
73
+ }
74
+
75
+ /**
76
+ * The constructional D-007 guard: a manifest source must resolve inside
77
+ * payload/ and may never name the kit's acceptance fixtures/ or tests/ —
78
+ * the engine refuses such entries rather than trusting manifest authors.
79
+ */
80
+ function assertInsidePayload(fromAbs, kitRoot, entryDesc) {
81
+ const payloadRoot = resolve(kitRoot, 'payload');
82
+ for (const forbidden of ['fixtures', 'tests']) {
83
+ if (within(fromAbs, resolve(kitRoot, forbidden))) {
84
+ throw new Error(`manifest entry ${entryDesc}: refuses to ship the kit's ${forbidden}/ (D-007 — acceptance fixtures and kit tests can never ship)`);
85
+ }
86
+ }
87
+ if (!within(fromAbs, payloadRoot)) {
88
+ throw new Error(`manifest entry ${entryDesc}: resolves outside payload/ (${fromAbs}) — the allowlist root is payload/ (D-007)`);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Root-dir name contract (§6): visible (never dotted), a single path
94
+ * segment, no traversal. The prompt lives in KK-19; the engine validates.
95
+ */
96
+ export function validateRootName(rootName) {
97
+ if (typeof rootName !== 'string' || rootName.length === 0) {
98
+ throw new SeedRefusal('root dir name must be a non-empty string');
99
+ }
100
+ if (rootName.startsWith('.')) {
101
+ throw new SeedRefusal(`root dir name ${JSON.stringify(rootName)} is dotted — the knowledge base is always visible, never hidden (PRD §6)`);
102
+ }
103
+ if (/[/\\]/.test(rootName) || rootName === '..' || isAbsolute(rootName)) {
104
+ throw new SeedRefusal(`root dir name ${JSON.stringify(rootName)} must be a single path segment (no separators)`);
105
+ }
106
+ return rootName;
107
+ }
108
+
109
+ // ----------------------------------------------------------------- manifest
110
+
111
+ /**
112
+ * Load + validate cli/kit.manifest.yaml. Every entry is checked against the
113
+ * D-007 constructional guards at LOAD time — an invalid manifest never
114
+ * reaches the copy phase. Returns the parsed manifest plus resolved roots.
115
+ */
116
+ export function loadManifest(kitRoot, manifestPath = join(kitRoot, MANIFEST_FILE)) {
117
+ let doc;
118
+ try {
119
+ doc = load(readFileSync(manifestPath, 'utf8'));
120
+ } catch (error) {
121
+ const detail = error instanceof YAMLException ? error.message : error.message;
122
+ throw new Error(`cannot load payload manifest ${manifestPath}: ${detail}`);
123
+ }
124
+ if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) {
125
+ throw new Error(`payload manifest ${manifestPath}: top level must be a mapping`);
126
+ }
127
+ if (doc['schema-version'] !== 1) {
128
+ throw new Error(`payload manifest ${manifestPath}: unsupported schema-version ${JSON.stringify(doc['schema-version'])}`);
129
+ }
130
+
131
+ const payloadRoot = resolve(kitRoot, 'payload');
132
+ const sections = { unconditional: doc.unconditional ?? {}, stacks: doc.stacks ?? {} };
133
+ for (const [group, bySection] of Object.entries(sections)) {
134
+ if (typeof bySection !== 'object' || bySection === null || Array.isArray(bySection)) {
135
+ throw new Error(`payload manifest: ${group} must be a mapping of sections to entry lists`);
136
+ }
137
+ for (const [section, entries] of Object.entries(bySection)) {
138
+ if (!Array.isArray(entries)) throw new Error(`payload manifest: ${group}.${section} must be a list`);
139
+ entries.forEach((entry, i) => {
140
+ const desc = `${group}.${section}[${i}]`;
141
+ if (typeof entry !== 'object' || entry === null) throw new Error(`manifest entry ${desc}: must be a { from, to } mapping`);
142
+ assertSanePath(entry.from, 'from', desc);
143
+ assertSanePath(entry.to, 'to', desc);
144
+ assertInsidePayload(resolve(payloadRoot, entry.from), kitRoot, desc);
145
+ });
146
+ }
147
+ }
148
+
149
+ const create = doc.create ?? [];
150
+ if (!Array.isArray(create)) throw new Error('payload manifest: create must be a list of target dirs');
151
+ create.forEach((dir, i) => assertSanePath(dir, 'dir', `create[${i}]`));
152
+
153
+ // Platform wrapper registry (KK-18): data-driven and extensible, but held
154
+ // to the same D-007 construction as copy entries — a template that escapes
155
+ // payload/ (or names fixtures/tests) refuses the manifest at load time.
156
+ // Targets are CLIENT-REPO-ROOT-relative conventional paths; they only need
157
+ // path sanity (relative, ..-free), not payload containment.
158
+ const platforms = doc.platforms ?? {};
159
+ if (typeof platforms !== 'object' || platforms === null || Array.isArray(platforms)) {
160
+ throw new Error('payload manifest: platforms must be a mapping of platform ids to wrapper specs');
161
+ }
162
+ const wrapperTargets = new Set();
163
+ for (const [id, spec] of Object.entries(platforms)) {
164
+ const desc = `platforms.${id}`;
165
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) {
166
+ throw new Error(`payload manifest: ${desc}: platform ids are lowercase kebab-case`);
167
+ }
168
+ if (typeof spec !== 'object' || spec === null || Array.isArray(spec)) {
169
+ throw new Error(`payload manifest: ${desc}: must be a { name, template, target, mode } mapping`);
170
+ }
171
+ if (typeof spec.name !== 'string' || spec.name.length === 0) {
172
+ throw new Error(`payload manifest: ${desc}: name must be a non-empty string`);
173
+ }
174
+ assertSanePath(spec.template, 'template', desc);
175
+ assertInsidePayload(resolve(payloadRoot, spec.template), kitRoot, desc);
176
+ assertSanePath(spec.target, 'target', desc);
177
+ if (spec.mode !== 'shared' && spec.mode !== 'dedicated') {
178
+ throw new Error(`payload manifest: ${desc}: mode must be "shared" or "dedicated" (§6 collision policy), got ${JSON.stringify(spec.mode)}`);
179
+ }
180
+ if (wrapperTargets.has(spec.target)) {
181
+ throw new Error(`payload manifest: ${desc}: duplicate wrapper target ${JSON.stringify(spec.target)}`);
182
+ }
183
+ wrapperTargets.add(spec.target);
184
+ }
185
+
186
+ const rootFiles = doc['root-files'] ?? [];
187
+ if (!Array.isArray(rootFiles)) throw new Error('payload manifest: root-files must be a list');
188
+ for (const name of rootFiles) {
189
+ if (!ROOT_FILE_ALLOWLIST.includes(name)) {
190
+ throw new Error(`payload manifest: root-files entry ${JSON.stringify(name)} is not in the sanctioned allowlist [${ROOT_FILE_ALLOWLIST.join(', ')}] (D-007)`);
191
+ }
192
+ }
193
+
194
+ const zones = doc.zones ?? {};
195
+ const defaults = doc.defaults ?? {};
196
+ return { manifestPath, kitRoot, payloadRoot, sections, platforms, create, rootFiles, zones, defaults };
197
+ }
198
+
199
+ /** Every file beneath dir (relative /-joined paths), sorted; symlinks refused. */
200
+ function walkFiles(dirAbs, relPrefix, entryDesc) {
201
+ const out = [];
202
+ for (const entry of readdirSync(dirAbs, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
203
+ const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
204
+ if (entry.isSymbolicLink()) {
205
+ throw new Error(`manifest entry ${entryDesc}: ${rel} is a symlink — payload sources must be regular files (D-007: a link could reach outside the allowlist)`);
206
+ }
207
+ if (entry.isDirectory()) out.push(...walkFiles(join(dirAbs, entry.name), rel, entryDesc));
208
+ else out.push(rel);
209
+ }
210
+ return out;
211
+ }
212
+
213
+ /**
214
+ * Expand the manifest for a stack selection into the exact copy plan:
215
+ * sorted [{ from (abs), to (seeded-root-relative) }]. Root-files are
216
+ * appended only when present at the kit root (required-at-publish, KK-28).
217
+ * Duplicate targets are a manifest bug and refuse.
218
+ */
219
+ export function expandManifest(manifest, stacks = []) {
220
+ const { payloadRoot, kitRoot, sections } = manifest;
221
+ const known = Object.keys(sections.stacks).sort();
222
+ for (const stack of stacks) {
223
+ if (!known.includes(stack)) {
224
+ throw new Error(`unknown stack ${JSON.stringify(stack)} — manifest defines: ${known.join(', ') || '(none)'}`);
225
+ }
226
+ }
227
+
228
+ const entries = [
229
+ ...Object.values(sections.unconditional).flat(),
230
+ ...[...new Set(stacks)].sort().flatMap((s) => sections.stacks[s]),
231
+ ];
232
+
233
+ const plan = [];
234
+ for (const entry of entries) {
235
+ const fromAbs = resolve(payloadRoot, entry.from);
236
+ if (!existsSync(fromAbs)) throw new Error(`manifest entry ${entry.from}: missing from payload/ — the allowlist and the payload tree have drifted`);
237
+ const stat = lstatSync(fromAbs);
238
+ if (stat.isSymbolicLink()) throw new Error(`manifest entry ${entry.from}: symlink sources are refused (D-007)`);
239
+ if (stat.isDirectory()) {
240
+ for (const rel of walkFiles(fromAbs, '', entry.from)) {
241
+ plan.push({ from: join(fromAbs, rel), to: `${entry.to}/${rel}` });
242
+ }
243
+ } else {
244
+ plan.push({ from: fromAbs, to: entry.to });
245
+ }
246
+ }
247
+ for (const name of manifest.rootFiles) {
248
+ const fromAbs = join(kitRoot, name);
249
+ if (existsSync(fromAbs)) plan.push({ from: fromAbs, to: name }); // required-at-publish (KK-28)
250
+ }
251
+
252
+ plan.sort((a, b) => a.to.localeCompare(b.to));
253
+ for (let i = 1; i < plan.length; i += 1) {
254
+ if (plan[i].to === plan[i - 1].to) throw new Error(`manifest expands to duplicate target ${plan[i].to}`);
255
+ }
256
+ const seededManifestClash = plan.find((p) => p.to === SEEDED_MANIFEST);
257
+ if (seededManifestClash) throw new Error(`manifest entry targets ${SEEDED_MANIFEST} — that file is generated by the engine (§9.1)`);
258
+ return plan;
259
+ }
260
+
261
+ // ------------------------------------------------------------------ seeding
262
+
263
+ /** Deterministic seeded kit.manifest.yaml (§9.1): stamp + zones + echo. */
264
+ function renderSeededManifest({ version, stacks, zones, files }) {
265
+ const list = (xs) => (xs.length ? `[${xs.join(', ')}]` : '[]');
266
+ const lines = [
267
+ '# kit.manifest.yaml — seeded by `unknown-knowledge init` (PRD §9.1).',
268
+ '# Kit version stamp (semver semantics per kit CHANGELOG, KK-28), the',
269
+ '# stacks selected at init (D-009), the zone map (kit-vendored vs. client',
270
+ '# data — everything is client-owned after seed, D-001), and the exact',
271
+ '# seeded file list (the D-007 allowlist echo). Presence of this file',
272
+ '# marks a seeded repo: v1 init refuses to re-seed (PRD §6).',
273
+ 'schema-version: 1',
274
+ `kit-version: "${version}"`,
275
+ `stacks: ${list(stacks)}`,
276
+ 'zones:',
277
+ ` seeded: ${list(zones.seeded ?? [])}`,
278
+ ` client: ${list(zones.client ?? [])}`,
279
+ 'files:',
280
+ ...files.map((f) => ` - ${f}`),
281
+ ];
282
+ return `${lines.join('\n')}\n`;
283
+ }
284
+
285
+ /**
286
+ * Seed a target repo from the manifest. Refuses (SeedRefusal) on a dotted
287
+ * root name or an existing target root — even a partial seed (§6).
288
+ *
289
+ * @param {object} options
290
+ * @param {string} options.kitRoot kit repo root (holds payload/ + cli/)
291
+ * @param {string} options.targetDir client repo root to seed into
292
+ * @param {string} [options.rootName] seeded dir name (default unknown-knowledge)
293
+ * @param {string[]} [options.stacks] selected stacks (manifest keys)
294
+ * @param {string} [options.manifestPath] override, for tests
295
+ * @returns {{ root: string, rootName: string, version: string, stacks: string[], files: string[] }}
296
+ */
297
+ export function copyPayload({ kitRoot, targetDir, rootName = DEFAULT_ROOT, stacks = [], manifestPath }) {
298
+ validateRootName(rootName);
299
+ const manifest = loadManifest(kitRoot, manifestPath);
300
+ const selected = [...new Set(stacks)].sort();
301
+ const plan = expandManifest(manifest, selected);
302
+
303
+ const destRoot = resolve(targetDir, rootName);
304
+ if (existsSync(destRoot)) {
305
+ throw new SeedRefusal(
306
+ `${destRoot} already exists — refusing to seed over it, even a partial seed. `
307
+ + 'Re-init/upgrade is deferred (PRD §11.1): move or remove the directory, or pass a different root name, then re-run.');
308
+ }
309
+ if (!existsSync(resolve(targetDir))) {
310
+ throw new Error(`target dir ${resolve(targetDir)} does not exist`);
311
+ }
312
+
313
+ const version = JSON.parse(readFileSync(join(kitRoot, 'package.json'), 'utf8')).version;
314
+
315
+ mkdirSync(destRoot, { recursive: true });
316
+ const files = [];
317
+ for (const { from, to } of plan) {
318
+ const dest = join(destRoot, to);
319
+ mkdirSync(dirname(dest), { recursive: true });
320
+ copyFileSync(from, dest);
321
+ files.push(to);
322
+ }
323
+ for (const dir of manifest.create) {
324
+ mkdirSync(join(destRoot, dir), { recursive: true });
325
+ writeFileSync(join(destRoot, dir, '.gitkeep'), '');
326
+ files.push(`${dir}/.gitkeep`);
327
+ }
328
+ files.push(SEEDED_MANIFEST);
329
+ files.sort();
330
+ writeFileSync(join(destRoot, SEEDED_MANIFEST),
331
+ renderSeededManifest({ version, stacks: selected, zones: manifest.zones, files }));
332
+
333
+ return { root: destRoot, rootName, version, stacks: selected, files };
334
+ }