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,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
|
+
}
|