phronesis 1.0.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 (142) hide show
  1. package/README.md +66 -0
  2. package/package.json +34 -0
  3. package/src/act.js +61 -0
  4. package/src/active-context.js +56 -0
  5. package/src/adapter.js +386 -0
  6. package/src/cli.js +1893 -0
  7. package/src/codex-hooks-schema.js +92 -0
  8. package/src/compile.js +513 -0
  9. package/src/doctor.js +303 -0
  10. package/src/due.js +271 -0
  11. package/src/eval.js +467 -0
  12. package/src/event.js +212 -0
  13. package/src/export.js +136 -0
  14. package/src/guard.js +89 -0
  15. package/src/hooks.js +230 -0
  16. package/src/ingest.js +965 -0
  17. package/src/init.js +150 -0
  18. package/src/layout.js +394 -0
  19. package/src/links.js +83 -0
  20. package/src/lint.js +58 -0
  21. package/src/profile.js +116 -0
  22. package/src/prompts.js +26 -0
  23. package/src/registry.js +190 -0
  24. package/src/scaffold.js +134 -0
  25. package/src/search.js +139 -0
  26. package/src/skill-discovery.js +117 -0
  27. package/src/skills-bump.js +885 -0
  28. package/src/skills.js +355 -0
  29. package/src/subscription.js +5 -0
  30. package/src/validate.js +152 -0
  31. package/templates/claude/settings.json +86 -0
  32. package/templates/codex/INDEX.md +68 -0
  33. package/templates/codex/seed/ai-practice/compile-dont-just-capture.md +42 -0
  34. package/templates/codex/seed/ai-practice/design-for-the-distribution.md +42 -0
  35. package/templates/codex/seed/ai-practice/from-spec-author-to-outcome-curator.md +43 -0
  36. package/templates/codex/seed/ai-practice/recall-builds-trust-when-calibrated.md +70 -0
  37. package/templates/codex/seed/ai-practice/the-eval-is-the-spec.md +42 -0
  38. package/templates/codex/seed/ai-practice/the-gold-dust-leaves-no-trace.md +72 -0
  39. package/templates/codex/seed/ai-practice/the-model-is-not-the-moat.md +40 -0
  40. package/templates/codex/seed/ai-practice/when-generation-is-cheap-judgment-is-the-work.md +39 -0
  41. package/templates/codex/seed/judgment/contextual-stewardship-is-the-senior-skill.md +50 -0
  42. package/templates/codex/seed/judgment/decision-quality-is-distinct-from-outcome-quality.md +39 -0
  43. package/templates/codex/seed/judgment/embrace-reality-ego-and-blind-spots.md +38 -0
  44. package/templates/codex/seed/judgment/groups-seek-consensus-individuals-seek-truth.md +44 -0
  45. package/templates/codex/seed/judgment/heresies-are-where-the-edge-lives.md +42 -0
  46. package/templates/codex/seed/judgment/judgment-compounds-through-honest-reflection.md +38 -0
  47. package/templates/codex/seed/judgment/phronesis-is-judgment-in-the-particular.md +67 -0
  48. package/templates/codex/seed/judgment/show-me-the-apparatus.md +40 -0
  49. package/templates/codex/seed/judgment/small-judgment-gaps-compound-under-leverage.md +37 -0
  50. package/templates/codex/seed/judgment/taste-is-compressed-judgment.md +38 -0
  51. package/templates/codex/seed/judgment/trust-taste-only-where-feedback-is-valid.md +42 -0
  52. package/templates/codex/seed/judgment/weigh-the-reasoning-not-the-recommendation.md +43 -0
  53. package/templates/codex/seed/leadership/a-managers-output-is-the-teams-output.md +35 -0
  54. package/templates/codex/seed/leadership/most-team-problems-are-structural.md +40 -0
  55. package/templates/codex/seed/leadership/next-play.md +36 -0
  56. package/templates/codex/seed/leadership/psychological-safety.md +39 -0
  57. package/templates/codex/seed/leadership/right-size-your-teams.md +38 -0
  58. package/templates/codex/seed/leadership/spend-time-on-high-leverage-activities.md +36 -0
  59. package/templates/codex/seed/leadership/talent-density-is-the-master-variable.md +39 -0
  60. package/templates/codex/seed/leadership/under-pressure-calm-is-contagious.md +36 -0
  61. package/templates/codex/seed/leadership/your-best-alternative-is-your-leverage.md +37 -0
  62. package/templates/codex/seed/pm/a-problem-well-stated-is-half-solved.md +39 -0
  63. package/templates/codex/seed/pm/a-real-strategy-makes-planning-trivial.md +40 -0
  64. package/templates/codex/seed/pm/barrels-and-ammunition.md +39 -0
  65. package/templates/codex/seed/pm/develop-taste-in-your-beliefs.md +38 -0
  66. package/templates/codex/seed/pm/hire-for-progress.md +44 -0
  67. package/templates/codex/seed/pm/jobs-are-stable-solutions-churn.md +39 -0
  68. package/templates/codex/seed/pm/level-by-the-ambiguity-someone-can-own.md +39 -0
  69. package/templates/codex/seed/pm/listen-before-you-prescribe.md +38 -0
  70. package/templates/codex/seed/pm/live-in-the-future-and-build-whats-missing.md +38 -0
  71. package/templates/codex/seed/pm/match-effort-to-leverage.md +37 -0
  72. package/templates/codex/seed/pm/not-my-fault-but-my-problem.md +40 -0
  73. package/templates/codex/seed/pm/pick-the-job-then-refuse-everything-outside-it.md +37 -0
  74. package/templates/codex/seed/pm/retention-sets-the-ceiling.md +38 -0
  75. package/templates/codex/seed/pm/scale-with-leaders-not-process.md +38 -0
  76. package/templates/codex/seed/pm/solution-discovery-over-problem-validation.md +38 -0
  77. package/templates/codex/seed/pm/tenfold-better-on-a-painful-problem.md +40 -0
  78. package/templates/codex-surface/README.md +73 -0
  79. package/templates/codex-surface/hooks/_resolve.sh +36 -0
  80. package/templates/codex-surface/hooks/recall-guard.sh +30 -0
  81. package/templates/codex-surface/hooks/session-start.sh +18 -0
  82. package/templates/codex-surface/hooks/session-sweep-precompact.sh +20 -0
  83. package/templates/codex-surface/hooks/session-sweep-subagent.sh +23 -0
  84. package/templates/codex-surface/hooks/session-sweep.sh +19 -0
  85. package/templates/codex-surface/hooks.json +69 -0
  86. package/templates/domains/pm/AGENTS.md +111 -0
  87. package/templates/domains/pm/MEMORY.md +7 -0
  88. package/templates/domains/pm/USER.md +8 -0
  89. package/templates/phronesis-hooks/compile-active-context.sh +38 -0
  90. package/templates/phronesis-hooks/recall-guard.sh +103 -0
  91. package/templates/phronesis-hooks/session-start.sh +90 -0
  92. package/templates/phronesis-hooks/session-sweep.sh +140 -0
  93. package/templates/phronesis-hooks/skill-lifecycle.sh +37 -0
  94. package/templates/skills/compile/SKILL.md +279 -0
  95. package/templates/skills/compile/evals/rubric.md +75 -0
  96. package/templates/skills/decision-log/SKILL.md +156 -0
  97. package/templates/skills/decision-log/evals/rubric.md +97 -0
  98. package/templates/skills/extract-gold-dust/SKILL.md +311 -0
  99. package/templates/skills/extract-gold-dust/evals/rubric.md +52 -0
  100. package/templates/skills/ingest/SKILL.md +204 -0
  101. package/templates/skills/ingest/evals/rubric.md +117 -0
  102. package/templates/skills/lint/SKILL.md +246 -0
  103. package/templates/skills/lint/evals/rubric.md +139 -0
  104. package/templates/skills/meeting-prep/SKILL.md +195 -0
  105. package/templates/skills/meeting-prep/evals/rubric.md +93 -0
  106. package/templates/skills/onboard/SKILL.md +258 -0
  107. package/templates/skills/onboard/evals/rubric.md +60 -0
  108. package/templates/skills/prd-draft/SKILL.md +193 -0
  109. package/templates/skills/prd-draft/evals/rubric.md +38 -0
  110. package/templates/skills/prep-extraction/SKILL.md +217 -0
  111. package/templates/skills/prep-extraction/evals/rubric.md +45 -0
  112. package/templates/skills/recall/SKILL.md +155 -0
  113. package/templates/skills/recall/evals/rubric.md +37 -0
  114. package/templates/skills/research/SKILL.md +247 -0
  115. package/templates/skills/research/evals/rubric.md +139 -0
  116. package/templates/skills/stakeholder-update/SKILL.md +163 -0
  117. package/templates/skills/stakeholder-update/evals/rubric.md +111 -0
  118. package/vendor/core/package.json +25 -0
  119. package/vendor/core/src/action-registry.js +459 -0
  120. package/vendor/core/src/action-writers.js +201 -0
  121. package/vendor/core/src/actions.js +2140 -0
  122. package/vendor/core/src/active-context.js +167 -0
  123. package/vendor/core/src/artifacts.js +301 -0
  124. package/vendor/core/src/content-digest.js +21 -0
  125. package/vendor/core/src/contract.js +245 -0
  126. package/vendor/core/src/event-log.js +338 -0
  127. package/vendor/core/src/guard.js +398 -0
  128. package/vendor/core/src/hook-executor.js +527 -0
  129. package/vendor/core/src/index.js +180 -0
  130. package/vendor/core/src/ingest.js +286 -0
  131. package/vendor/core/src/install-root.js +42 -0
  132. package/vendor/core/src/layout.js +13 -0
  133. package/vendor/core/src/link-index.js +198 -0
  134. package/vendor/core/src/lint.js +368 -0
  135. package/vendor/core/src/schedule.js +535 -0
  136. package/vendor/core/src/search.js +534 -0
  137. package/vendor/core/src/session-hooks.js +129 -0
  138. package/vendor/core/src/skill-lifecycle.js +479 -0
  139. package/vendor/core/src/skill-registry.js +524 -0
  140. package/vendor/core/src/subscription.js +84 -0
  141. package/vendor/core/src/validate-object.js +62 -0
  142. package/vendor/core/src/workspace-scan.js +125 -0
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@phronesis/core",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Phronesis shared substrate — the ontology contract, workspace scan, link index, event log, and install-root discovery. Imported by every surface (CLI, daemon, GUI); imports from no app.",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node --test test/*.test.js"
12
+ },
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "files": [
17
+ "src"
18
+ ],
19
+ "dependencies": {
20
+ "fs-extra": "^11.3.3",
21
+ "js-yaml": "^4.1.0",
22
+ "lunr": "^2.3.9"
23
+ },
24
+ "license": "MIT"
25
+ }
@@ -0,0 +1,459 @@
1
+ // The action-type registry layer (core-0011 / T2.4; specs/ontology.md §"Action type
2
+ // contract" :167; specs/trust-scopes.md Scope 1; per-packet decisions G1–G6 in
3
+ // harness/tasks/core-0011-action-registry.md).
4
+ //
5
+ // T2.2 hardcoded the action-type table (schema + permission default + target layout)
6
+ // in actions.js; this module externalizes it to the install's operator-editable
7
+ // `.phronesis/registry.json` `action_types` section — WITHIN A SAFETY LATTICE WHOSE
8
+ // FLOOR STAYS IN CODE. The split that matters:
9
+ //
10
+ // code-resident (trusted baseline, compiled into @phronesis/core):
11
+ // handlers (prepare/build), newFileOnly, the approval FLOOR (`approvalFloor` on
12
+ // record_outcome / promote_to_codex), and the default table the scaffold section
13
+ // is GENERATED from (one source — scaffold and fallback cannot drift).
14
+ // registry-resident (operator-editable, validated against the floor at load):
15
+ // per-type input schema (the D2 field-spec shape, serialized verbatim as JSON —
16
+ // G2: no ajv), permission, target_layout, reserved per-action hook slots (the
17
+ // V1 executor consumes the top-level hooks array instead — core-0013 H1).
18
+ //
19
+ // Every function here is pure over its arguments (the code table is a parameter, the
20
+ // bound convenience exports live in actions.js) — no disk, no install state.
21
+ //
22
+ // The lattice (G4/G5/G6):
23
+ // permission auto-run < ask-once-if-new < requires-approval. Below an action's
24
+ // code floor → refuse at load (no escalation channel in V1). Non-floor
25
+ // actions may take any of the three values — the baseline is the FLOOR,
26
+ // not the default. Removing an entry unregisters the type (it refuses
27
+ // to run): removal is infinite tightening.
28
+ // schema tighten-only: drop an optional field, narrow an enum, promote
29
+ // optional → required, add nonEmpty. Never: add a field the handler
30
+ // doesn't know (build() would silently drop it — the D2/null lesson),
31
+ // remove/un-require a required field, change a kind, widen an enum.
32
+ // layout fixed contract in V1: must equal the code layout at load (canonical
33
+ // paths are what TYPE_DIR, export, and portability stand on); at run,
34
+ // the computed relPath must match the pattern — checked contract, not
35
+ // decoration.
36
+ //
37
+ // Failure policy (G3): an ABSENT action_types section means a pre-T2.4 registry —
38
+ // the compiled defaults apply (safe: the fallback IS the code table, so absence can
39
+ // never loosen anything). A PRESENT-but-invalid section fails closed ALL-OR-NOTHING:
40
+ // one garbled entry disables the whole action layer with an error naming it. A config
41
+ // error is loud and total, never partial.
42
+
43
+ import { SLUG_SOURCE } from './contract.js';
44
+
45
+ export const PERMISSION_ORDER = ['auto-run', 'ask-once-if-new', 'requires-approval'];
46
+
47
+ // Strict top-level key set (G1): a typo'd section name (`action_type:`) must fail
48
+ // closed, not silently restore defaults — the gate condition matches the invariant,
49
+ // not "whatever parses". `review_intensity` is RESERVED, not consumed: the ratified
50
+ // specs promise the operator the key in this file (trust-scopes.md Scope 2), so
51
+ // strictness must not brick an install that follows the published spec — it sits
52
+ // inert until its consumer (Scope 2 review surfaces) ships. `hooks` graduated from
53
+ // that reserved class to CONSUMED in core-0013 (hook-executor.js resolveHooks —
54
+ // hooks.md §"Hook registry format"); `skills` graduated in core-0015 (skill-registry.js
55
+ // resolveSkillsSection — skills.md §"Skill registry"). Neither is validated HERE:
56
+ // hooks and skills are separate failure domains with their own loaders.
57
+ const REGISTRY_TOP_KEYS = [
58
+ 'schema_version',
59
+ 'created_by',
60
+ 'domains',
61
+ 'action_types',
62
+ 'hooks',
63
+ 'review_intensity',
64
+ 'skills',
65
+ ];
66
+ // Exported for the other registry-section loaders (skill-registry.js) — the known
67
+ // version set is one source, never a re-typed copy that drifts. v2 landed with the
68
+ // T5.1 compile handler (core-0017): the deferred-shaped v1 compile entry is rewritten
69
+ // to the real handler shape and the version bumped, keyed on this field (H6 migration).
70
+ export const REGISTRY_SCHEMA_VERSIONS = [0, 1, 2];
71
+
72
+ // `note` is deliberately NOT an entry key: the per-type display annotation is
73
+ // code-owned (status prints it from the code table). Serializing it would create a
74
+ // field the loader validates-then-ignores — the silent intent-drop this module
75
+ // refuses everywhere else.
76
+ // `target_layout_by_object_type` is the compile handler's per-object_type layout map
77
+ // (core-0017 H2b): compile writes two types (insight | organization) from one action,
78
+ // so its canonical layout is a map keyed by the payload discriminator, not a single
79
+ // string. Only the compile entry carries it; checkEntry refuses it elsewhere.
80
+ const ENTRY_KEYS = [
81
+ 'permission',
82
+ 'target_layout',
83
+ 'target_layout_by_object_type',
84
+ 'schema',
85
+ 'hooks',
86
+ 'deferred_to',
87
+ ];
88
+ const FIELD_SPEC_KEYS = ['kind', 'required', 'nonEmpty', 'values'];
89
+ // `paths` (core-0017) is a list of install-relative source-path strings — the compile
90
+ // handler's source_paths[]. It carries no nonEmpty/values/enum facets; required is the
91
+ // only facet that applies (the list itself must be present and non-empty — enforced in
92
+ // the handler's checkField, not as a per-element string facet).
93
+ const FIELD_KINDS = ['string', 'slug', 'date', 'enum', 'links', 'paths'];
94
+
95
+ function isPlainObject(v) {
96
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
97
+ }
98
+
99
+ // Serialize one D2 field spec to its JSON-clean registry form: known keys only, enum
100
+ // values copied (never a live reference into the code table).
101
+ function serializeFieldSpec(spec) {
102
+ const out = { kind: spec.kind };
103
+ if (spec.required === true) out.required = true;
104
+ if (spec.nonEmpty === true) out.nonEmpty = true;
105
+ if (spec.values !== undefined) out.values = [...spec.values];
106
+ return out;
107
+ }
108
+
109
+ // The scaffold's action_types section, generated FROM the code table — the single
110
+ // source the registry file is born from and the shape resolveActionTypes validates.
111
+ export function buildActionTypesSection(codeTable) {
112
+ const section = {};
113
+ for (const [type, entry] of Object.entries(codeTable)) {
114
+ const out = { permission: entry.permission };
115
+ // A handler declares EITHER a single canonical layout or (compile, H2b) a
116
+ // per-object_type map — never both. The scaffold serializes whichever it holds.
117
+ if (entry.targetLayoutByObjectType) {
118
+ out.target_layout_by_object_type = { ...entry.targetLayoutByObjectType };
119
+ } else {
120
+ out.target_layout = entry.targetLayout;
121
+ }
122
+ if (entry.deferredTo) out.deferred_to = entry.deferredTo;
123
+ if (entry.schema) {
124
+ out.schema = {};
125
+ for (const [field, spec] of Object.entries(entry.schema)) {
126
+ out.schema[field] = serializeFieldSpec(spec);
127
+ }
128
+ }
129
+ // Per-action hook-binding slots: RESERVED, empty-only (core-0013 H1). The V1
130
+ // executor consumes the top-level `hooks` subscription array — a per-action
131
+ // binding is expressible as a subscription (action_type.requested/committed +
132
+ // a payload.action_type filter), so these slots have no consumer; a non-empty
133
+ // binding fails closed at load (a binding nothing honors is silent intent-drop).
134
+ out.hooks = { pre: [], post: [] };
135
+ section[type] = out;
136
+ }
137
+ return section;
138
+ }
139
+
140
+ // G5 — registry schemas are tighten-only against the code schema.
141
+ function checkSchemaOverride(type, regSchema, codeSchema, errors) {
142
+ if (!isPlainObject(regSchema)) {
143
+ errors.push(`${type}.schema: must be an object of field specs`);
144
+ return;
145
+ }
146
+ for (const [field, spec] of Object.entries(regSchema)) {
147
+ // Own-property lookup, never the prototype chain: a field named `constructor`
148
+ // or `toString` must fail closed as unknown, not resolve to inherited junk.
149
+ if (!Object.hasOwn(codeSchema, field)) {
150
+ errors.push(
151
+ `${type}.schema.${field}: not a field the ${type} handler knows — the handler would silently drop it (add fields via a spec change, not the registry)`,
152
+ );
153
+ continue;
154
+ }
155
+ const code = codeSchema[field];
156
+ if (!isPlainObject(spec)) {
157
+ errors.push(`${type}.schema.${field}: must be a field-spec object`);
158
+ continue;
159
+ }
160
+ const unknown = Object.keys(spec).filter((k) => !FIELD_SPEC_KEYS.includes(k));
161
+ if (unknown.length) {
162
+ errors.push(`${type}.schema.${field}: unknown spec key(s): ${unknown.join(', ')}`);
163
+ }
164
+ if (!FIELD_KINDS.includes(spec.kind)) {
165
+ errors.push(`${type}.schema.${field}: kind "${spec.kind}" is not a D2 field kind (${FIELD_KINDS.join(' | ')})`);
166
+ } else if (spec.kind !== code.kind) {
167
+ errors.push(`${type}.schema.${field}: kind "${spec.kind}" differs from the handler's "${code.kind}" — kinds are not overridable`);
168
+ }
169
+ if (spec.required !== undefined && typeof spec.required !== 'boolean') {
170
+ errors.push(`${type}.schema.${field}: required must be a boolean`);
171
+ }
172
+ if (code.required === true && spec.required !== true) {
173
+ errors.push(`${type}.schema.${field}: the handler requires this field — required: true cannot be removed`);
174
+ }
175
+ if (spec.nonEmpty !== undefined && typeof spec.nonEmpty !== 'boolean') {
176
+ errors.push(`${type}.schema.${field}: nonEmpty must be a boolean`);
177
+ } else if (spec.nonEmpty === true && code.kind !== 'string') {
178
+ errors.push(`${type}.schema.${field}: nonEmpty only applies to string fields`);
179
+ }
180
+ if (code.nonEmpty === true && spec.nonEmpty !== true) {
181
+ errors.push(`${type}.schema.${field}: the handler refuses empty strings here — nonEmpty: true cannot be removed`);
182
+ }
183
+ if (code.kind === 'enum') {
184
+ if (!Array.isArray(spec.values) || spec.values.length === 0) {
185
+ errors.push(`${type}.schema.${field}: an enum field needs a non-empty values list`);
186
+ } else {
187
+ const widened = spec.values.filter((v) => !code.values.includes(v));
188
+ if (widened.length) {
189
+ errors.push(
190
+ `${type}.schema.${field}: values ${widened.join(', ')} are outside the handler's enum — enums may narrow, never widen`,
191
+ );
192
+ }
193
+ }
194
+ } else if (spec.values !== undefined) {
195
+ errors.push(`${type}.schema.${field}: values only applies to enum fields`);
196
+ }
197
+ }
198
+ for (const [field, code] of Object.entries(codeSchema)) {
199
+ if (code.required === true && regSchema[field] === undefined) {
200
+ errors.push(`${type}.schema: required field "${field}" is missing — the handler cannot run without it`);
201
+ }
202
+ // Dropping an OPTIONAL field is a legitimate tightening: payloads carrying it
203
+ // are then refused as unknown (fail closed), never silently accepted-and-ignored.
204
+ }
205
+ }
206
+
207
+ // G6 for compile's H2b map: the object_type set is fixed and each value must equal the
208
+ // canonical layout. Binding the discriminator to the path here is what kills the
209
+ // "registry pattern accepts a path the object_type contradicts" bug class the v3
210
+ // alternation-string idea would have admitted (core-0017 H2b) — drift fails closed.
211
+ function checkTargetLayoutMap(type, map, codeMap, errors) {
212
+ if (!isPlainObject(map)) {
213
+ errors.push(`${type}.target_layout_by_object_type: must be an object mapping object_type → canonical layout`);
214
+ return;
215
+ }
216
+ const codeKeys = Object.keys(codeMap).sort();
217
+ const regKeys = Object.keys(map).sort();
218
+ if (regKeys.join(',') !== codeKeys.join(',')) {
219
+ errors.push(
220
+ `${type}.target_layout_by_object_type: object_type set [${regKeys.join(', ') || '∅'}] differs from the canonical [${codeKeys.join(', ')}] — fixed contract`,
221
+ );
222
+ return;
223
+ }
224
+ for (const k of codeKeys) {
225
+ if (map[k] !== codeMap[k]) {
226
+ errors.push(
227
+ `${type}.target_layout_by_object_type.${k}: "${map[k]}" differs from the canonical layout "${codeMap[k]}" — layouts are fixed contract in V1`,
228
+ );
229
+ }
230
+ }
231
+ }
232
+
233
+ function checkHooks(type, hooks, errors) {
234
+ if (hooks === undefined) return;
235
+ if (!isPlainObject(hooks)) {
236
+ errors.push(`${type}.hooks: must be an object with pre/post lists`);
237
+ return;
238
+ }
239
+ const unknown = Object.keys(hooks).filter((k) => !['pre', 'post'].includes(k));
240
+ if (unknown.length) {
241
+ errors.push(`${type}.hooks: unknown key(s): ${unknown.join(', ')}`);
242
+ }
243
+ for (const phase of ['pre', 'post']) {
244
+ if (hooks[phase] === undefined) continue;
245
+ if (!Array.isArray(hooks[phase])) {
246
+ errors.push(`${type}.hooks.${phase}: must be a list`);
247
+ } else if (hooks[phase].length) {
248
+ errors.push(
249
+ `${type}.hooks.${phase}: per-action pre/post bindings have no consumer — the V1 executor reads the TOP-LEVEL hooks array (subscribe to action_type.requested/committed with a payload.action_type filter); a binding nothing honors is silent intent-drop`,
250
+ );
251
+ }
252
+ }
253
+ }
254
+
255
+ function checkEntry(type, entry, codeEntry, errors) {
256
+ if (!isPlainObject(entry)) {
257
+ errors.push(`${type}: must be an object`);
258
+ return;
259
+ }
260
+ const unknown = Object.keys(entry).filter((k) => !ENTRY_KEYS.includes(k));
261
+ if (unknown.length) {
262
+ errors.push(`${type}: unknown key(s): ${unknown.join(', ')}`);
263
+ }
264
+ // Layout is fixed contract in V1 (G6): relocating canonical state is a spec change,
265
+ // not a config flip — TYPE_DIR, the validator, and export all stand on these paths.
266
+ // compile (H2b) declares a per-object_type MAP instead of a single string; validate
267
+ // whichever the code table declares and refuse the wrong shape for this type.
268
+ if (codeEntry.targetLayoutByObjectType) {
269
+ if (entry.target_layout !== undefined) {
270
+ errors.push(
271
+ `${type}.target_layout: ${type} uses target_layout_by_object_type (a per-object_type map), not a single target_layout`,
272
+ );
273
+ }
274
+ checkTargetLayoutMap(type, entry.target_layout_by_object_type, codeEntry.targetLayoutByObjectType, errors);
275
+ } else {
276
+ if (entry.target_layout_by_object_type !== undefined) {
277
+ errors.push(
278
+ `${type}.target_layout_by_object_type: only the compile handler carries a per-object_type layout map`,
279
+ );
280
+ }
281
+ if (entry.target_layout !== codeEntry.targetLayout) {
282
+ errors.push(
283
+ `${type}.target_layout: "${entry.target_layout}" differs from the canonical layout "${codeEntry.targetLayout}" — layouts are fixed contract in V1`,
284
+ );
285
+ }
286
+ }
287
+ checkHooks(type, entry.hooks, errors);
288
+
289
+ if (codeEntry.deferredTo) {
290
+ // A deferred type (compile → T5.1) is registered but not runnable: its
291
+ // descriptive permission is recorded verbatim, never lattice-checked — the
292
+ // deferral refusal fires before any gate could read it.
293
+ if (typeof entry.permission !== 'string' || entry.permission.trim() === '') {
294
+ errors.push(`${type}.permission: must be a non-empty string`);
295
+ }
296
+ if (entry.deferred_to !== codeEntry.deferredTo) {
297
+ errors.push(`${type}.deferred_to: must be "${codeEntry.deferredTo}" (the handler is code-deferred)`);
298
+ }
299
+ if (entry.schema !== undefined) {
300
+ errors.push(`${type}.schema: a deferred type has no runnable schema yet`);
301
+ }
302
+ return;
303
+ }
304
+
305
+ if (entry.deferred_to !== undefined) {
306
+ errors.push(`${type}.deferred_to: only code-deferred types carry this (deferral is not operator-settable)`);
307
+ }
308
+ if (!PERMISSION_ORDER.includes(entry.permission)) {
309
+ errors.push(
310
+ `${type}.permission: "${entry.permission}" is not in the permission vocabulary (${PERMISSION_ORDER.join(' | ')})`,
311
+ );
312
+ } else if (
313
+ codeEntry.approvalFloor &&
314
+ PERMISSION_ORDER.indexOf(entry.permission) < PERMISSION_ORDER.indexOf('requires-approval')
315
+ ) {
316
+ // The lattice bite (G4): the comparison baseline is the CODE floor, never the
317
+ // registry's own prior value. Refused outright — no escalation channel in V1.
318
+ errors.push(
319
+ `${type}.permission: "${entry.permission}" loosens a code-floor approval-required action — the floor is compiled into @phronesis/core and is not registry-overridable`,
320
+ );
321
+ }
322
+ if (entry.schema === undefined) {
323
+ errors.push(`${type}.schema: required — the registry is what payloads validate against`);
324
+ } else {
325
+ checkSchemaOverride(type, entry.schema, codeEntry.schema, errors);
326
+ }
327
+ }
328
+
329
+ // Parse one registry field-spec back into the live D2 shape validatePayload consumes.
330
+ // (Today the shapes coincide; the explicit pick keeps an extra registry key from ever
331
+ // reaching checkField unvalidated.)
332
+ function liveFieldSpec(spec) {
333
+ const out = { kind: spec.kind };
334
+ if (spec.required === true) out.required = true;
335
+ if (spec.nonEmpty === true) out.nonEmpty = true;
336
+ if (spec.values !== undefined) out.values = [...spec.values];
337
+ return out;
338
+ }
339
+
340
+ // Resolve the EFFECTIVE action-type table for one install: the registry's schema /
341
+ // permission / layout over the code table's handlers. Returns:
342
+ // { errors, source, effective }
343
+ // errors non-empty → the whole action layer is invalid (all-or-nothing, G3)
344
+ // source 'registry' (section present and valid) | 'defaults' (absent section)
345
+ // effective type → { ...codeEntry, schema, permission, targetLayout } — a type
346
+ // whose entry was removed from a present section is ABSENT here
347
+ // (unregistered: it refuses to run; removal is infinite tightening)
348
+ export function resolveActionTypesAgainst({ registry, codeTable }) {
349
+ const errors = [];
350
+ if (!isPlainObject(registry)) {
351
+ return { errors: ['.phronesis/registry.json must be a JSON object'], source: 'defaults', effective: {} };
352
+ }
353
+ const unknownTop = Object.keys(registry).filter((k) => !REGISTRY_TOP_KEYS.includes(k));
354
+ if (unknownTop.length) {
355
+ errors.push(
356
+ `unknown top-level key(s) in .phronesis/registry.json: ${unknownTop.join(', ')} (allowed: ${REGISTRY_TOP_KEYS.join(', ')})`,
357
+ );
358
+ }
359
+ // The version is READ, not just written: a future shape change (the T5.1 compile
360
+ // handler will obsolete today's deferred-shaped compile entry) keys its migration
361
+ // on this field — an unversionable config file makes every upgrade a surprise.
362
+ if (!REGISTRY_SCHEMA_VERSIONS.includes(registry.schema_version)) {
363
+ errors.push(
364
+ `schema_version "${registry.schema_version}" is not a known registry version (${REGISTRY_SCHEMA_VERSIONS.join(', ')})`,
365
+ );
366
+ }
367
+ // The domain list is part of what the action layer loads: every action's domain
368
+ // resolution depends on it, so a malformed list fails closed HERE — the one
369
+ // validator both `act` and `registry status` consume (a status that calls the
370
+ // layer disabled while act still writes is the two-matchers drift, rules.md).
371
+ if (!Array.isArray(registry.domains) || registry.domains.some((d) => typeof d !== 'string')) {
372
+ errors.push('domains: must be a list of domain-slug strings (every action resolves its domain against it)');
373
+ }
374
+
375
+ const section = registry.action_types;
376
+ // A section under schema_version 0 is contradictory state: version 0 predates the
377
+ // section, and the version field is what future migrations key on (the T5.1
378
+ // obligation) — v1-shaped content under v0 would make that keying meaningless.
379
+ if (section !== undefined && registry.schema_version === 0) {
380
+ errors.push(
381
+ 'action_types requires schema_version 1 — a version-0 registry predates the section (bump the version, or remove the section to keep built-in defaults)',
382
+ );
383
+ }
384
+ if (section === undefined) {
385
+ // Pre-T2.4 registry (G3): compiled defaults. Safe — the fallback IS the code
386
+ // table, so an absent section can never loosen anything.
387
+ return { errors, source: 'defaults', effective: errors.length ? {} : { ...codeTable } };
388
+ }
389
+
390
+ if (!isPlainObject(section)) {
391
+ errors.push('action_types: must be an object mapping type → entry');
392
+ return { errors, source: 'registry', effective: {} };
393
+ }
394
+ // core-0017 H6 — fail closed RECOVERABLY when the compile handler has landed (the code
395
+ // table's compile is no longer deferred) but this install still carries the pre-T5.1
396
+ // deferred-shaped compile entry. Without this, that stale entry fails every
397
+ // non-deferred check at once (a descriptive permission, no schema, a deferred_to the
398
+ // code no longer declares) and disables the WHOLE action layer on a routine
399
+ // @phronesis/core upgrade. One recoverable instruction beats the generic error wall.
400
+ if (
401
+ isPlainObject(section.compile) &&
402
+ section.compile.deferred_to !== undefined &&
403
+ codeTable.compile &&
404
+ !codeTable.compile.deferredTo
405
+ ) {
406
+ errors.push(
407
+ 'action_types.compile is the pre-T5.1 deferred entry, but the compile handler has landed — run the compile-handler migration (`node harness/migrations/0017-compile-handler.mjs <install>`) to rewrite it to the real schema and bump schema_version to 2',
408
+ );
409
+ return { errors, source: 'registry', effective: {} };
410
+ }
411
+ for (const type of Object.keys(section)) {
412
+ // Own-property lookup (an entry named `constructor`/`toString` must hit this
413
+ // refusal, not resolve up the prototype chain and skip validation entirely).
414
+ if (!Object.hasOwn(codeTable, type)) {
415
+ errors.push(
416
+ `action_types.${type}: no such action type in @phronesis/core — a registry entry cannot conjure a handler (Action Sprawl stays resisted)`,
417
+ );
418
+ }
419
+ }
420
+ const effective = {};
421
+ for (const [type, codeEntry] of Object.entries(codeTable)) {
422
+ const entry = section[type];
423
+ if (entry === undefined) continue; // unregistered — refuses to run
424
+ checkEntry(type, entry, codeEntry, errors);
425
+ if (errors.length) continue; // shape only matters when everything is valid
426
+ const merged = { ...codeEntry, permission: entry.permission };
427
+ if (codeEntry.targetLayoutByObjectType) {
428
+ merged.targetLayoutByObjectType = { ...entry.target_layout_by_object_type };
429
+ } else {
430
+ merged.targetLayout = entry.target_layout;
431
+ }
432
+ if (entry.schema !== undefined) {
433
+ merged.schema = {};
434
+ for (const [field, spec] of Object.entries(entry.schema)) {
435
+ merged.schema[field] = liveFieldSpec(spec);
436
+ }
437
+ }
438
+ effective[type] = merged;
439
+ }
440
+ if (errors.length) return { errors, source: 'registry', effective: {} };
441
+ return { errors, source: 'registry', effective };
442
+ }
443
+
444
+ function escapeRegExp(s) {
445
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
446
+ }
447
+
448
+ // Compile a target_layout into the pattern the computed relPath must match (G6 —
449
+ // checked contract, not decoration): {domain} is the RESOLVED domain literal,
450
+ // {slug}/{name} are the one slug grammar (contract.js — never a re-typed copy),
451
+ // YYYY-MM-DD is a date stamp.
452
+ export function layoutPattern(layout, { domain }) {
453
+ const re = escapeRegExp(layout)
454
+ .replaceAll('\\{domain\\}', escapeRegExp(domain))
455
+ .replaceAll('\\{slug\\}', SLUG_SOURCE)
456
+ .replaceAll('\\{name\\}', SLUG_SOURCE)
457
+ .replaceAll('YYYY-MM-DD', '\\d{4}-\\d{2}-\\d{2}');
458
+ return new RegExp(`^${re}$`);
459
+ }