@dzhechkov/harness-core 0.8.11 → 0.8.21

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 (205) hide show
  1. package/.dz-manifest.json +399 -139
  2. package/LICENSE +13 -0
  3. package/README.md +420 -6
  4. package/dist/agents-policy.d.ts +15 -1
  5. package/dist/agents-policy.d.ts.map +1 -1
  6. package/dist/agents-policy.js +27 -1
  7. package/dist/agents-policy.js.map +1 -1
  8. package/dist/amendment-trace.d.ts +72 -0
  9. package/dist/amendment-trace.d.ts.map +1 -1
  10. package/dist/amendment-trace.js +385 -17
  11. package/dist/amendment-trace.js.map +1 -1
  12. package/dist/backlog-public.d.ts +153 -0
  13. package/dist/backlog-public.d.ts.map +1 -0
  14. package/dist/backlog-public.js +415 -0
  15. package/dist/backlog-public.js.map +1 -0
  16. package/dist/backlog-transitions.d.ts +48 -0
  17. package/dist/backlog-transitions.d.ts.map +1 -0
  18. package/dist/backlog-transitions.js +64 -0
  19. package/dist/backlog-transitions.js.map +1 -0
  20. package/dist/backlog.d.ts.map +1 -1
  21. package/dist/backlog.js +13 -0
  22. package/dist/backlog.js.map +1 -1
  23. package/dist/claim-check.d.ts.map +1 -1
  24. package/dist/claim-check.js +24 -2
  25. package/dist/claim-check.js.map +1 -1
  26. package/dist/claude-hooks-assets.d.ts +93 -0
  27. package/dist/claude-hooks-assets.d.ts.map +1 -0
  28. package/dist/claude-hooks-assets.js +208 -0
  29. package/dist/claude-hooks-assets.js.map +1 -0
  30. package/dist/clean-room-smoke.d.ts +83 -0
  31. package/dist/clean-room-smoke.d.ts.map +1 -0
  32. package/dist/clean-room-smoke.js +138 -0
  33. package/dist/clean-room-smoke.js.map +1 -0
  34. package/dist/cmd-usage.d.ts.map +1 -1
  35. package/dist/cmd-usage.js +36 -6
  36. package/dist/cmd-usage.js.map +1 -1
  37. package/dist/codex-hooks-assets.d.ts +25 -7
  38. package/dist/codex-hooks-assets.d.ts.map +1 -1
  39. package/dist/codex-hooks-assets.js +138 -30
  40. package/dist/codex-hooks-assets.js.map +1 -1
  41. package/dist/codex-hooks.d.ts +21 -1
  42. package/dist/codex-hooks.d.ts.map +1 -1
  43. package/dist/codex-hooks.js +21 -1
  44. package/dist/codex-hooks.js.map +1 -1
  45. package/dist/course-staleness.d.ts +19 -0
  46. package/dist/course-staleness.d.ts.map +1 -0
  47. package/dist/course-staleness.js +95 -0
  48. package/dist/course-staleness.js.map +1 -0
  49. package/dist/destructive-guard-hook.d.ts +40 -0
  50. package/dist/destructive-guard-hook.d.ts.map +1 -0
  51. package/dist/destructive-guard-hook.js +109 -0
  52. package/dist/destructive-guard-hook.js.map +1 -0
  53. package/dist/destructive-guard.d.ts +27 -0
  54. package/dist/destructive-guard.d.ts.map +1 -0
  55. package/dist/destructive-guard.js +2808 -0
  56. package/dist/destructive-guard.js.map +1 -0
  57. package/dist/discrimination-gate.d.ts +28 -3
  58. package/dist/discrimination-gate.d.ts.map +1 -1
  59. package/dist/discrimination-gate.js +76 -16
  60. package/dist/discrimination-gate.js.map +1 -1
  61. package/dist/feature-adr-routing.d.ts +95 -1
  62. package/dist/feature-adr-routing.d.ts.map +1 -1
  63. package/dist/feature-adr-routing.js +193 -27
  64. package/dist/feature-adr-routing.js.map +1 -1
  65. package/dist/guard.d.ts +90 -0
  66. package/dist/guard.d.ts.map +1 -1
  67. package/dist/guard.js +271 -0
  68. package/dist/guard.js.map +1 -1
  69. package/dist/harness-core-location.d.ts +18 -0
  70. package/dist/harness-core-location.d.ts.map +1 -0
  71. package/dist/harness-core-location.js +42 -0
  72. package/dist/harness-core-location.js.map +1 -0
  73. package/dist/index.d.ts +24 -6
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +24 -3
  76. package/dist/index.js.map +1 -1
  77. package/dist/lead-shift-gate.d.ts +95 -0
  78. package/dist/lead-shift-gate.d.ts.map +1 -0
  79. package/dist/lead-shift-gate.js +100 -0
  80. package/dist/lead-shift-gate.js.map +1 -0
  81. package/dist/ledger-backfill.d.ts +11 -1
  82. package/dist/ledger-backfill.d.ts.map +1 -1
  83. package/dist/ledger-backfill.js +19 -0
  84. package/dist/ledger-backfill.js.map +1 -1
  85. package/dist/loop-blobs.generated.d.ts +1 -1
  86. package/dist/loop-blobs.generated.d.ts.map +1 -1
  87. package/dist/loop-blobs.generated.js +12 -3
  88. package/dist/loop-blobs.generated.js.map +1 -1
  89. package/dist/loop-lint.d.ts.map +1 -1
  90. package/dist/loop-lint.js +56 -7
  91. package/dist/loop-lint.js.map +1 -1
  92. package/dist/loop-plan-graph.d.ts +1 -3
  93. package/dist/loop-plan-graph.d.ts.map +1 -1
  94. package/dist/loop-plan-graph.js +70 -1
  95. package/dist/loop-plan-graph.js.map +1 -1
  96. package/dist/loop-trace.d.ts.map +1 -1
  97. package/dist/loop-trace.js +16 -2
  98. package/dist/loop-trace.js.map +1 -1
  99. package/dist/managed-hooks.d.ts +5 -6
  100. package/dist/managed-hooks.d.ts.map +1 -1
  101. package/dist/managed-hooks.js +2 -2
  102. package/dist/managed-hooks.js.map +1 -1
  103. package/dist/model-recommender.d.ts.map +1 -1
  104. package/dist/model-recommender.js +14 -3
  105. package/dist/model-recommender.js.map +1 -1
  106. package/dist/operations.d.ts.map +1 -1
  107. package/dist/operations.js +82 -0
  108. package/dist/operations.js.map +1 -1
  109. package/dist/patterns.d.ts +23 -0
  110. package/dist/patterns.d.ts.map +1 -1
  111. package/dist/patterns.js +10 -0
  112. package/dist/patterns.js.map +1 -1
  113. package/dist/publish.d.ts +11 -0
  114. package/dist/publish.d.ts.map +1 -1
  115. package/dist/publish.js +16 -2
  116. package/dist/publish.js.map +1 -1
  117. package/dist/registry.d.ts.map +1 -1
  118. package/dist/registry.js +3 -2
  119. package/dist/registry.js.map +1 -1
  120. package/dist/score.d.ts.map +1 -1
  121. package/dist/score.js +15 -4
  122. package/dist/score.js.map +1 -1
  123. package/dist/session-retro.d.ts +121 -2
  124. package/dist/session-retro.d.ts.map +1 -1
  125. package/dist/session-retro.js +454 -11
  126. package/dist/session-retro.js.map +1 -1
  127. package/dist/setup.d.ts +28 -0
  128. package/dist/setup.d.ts.map +1 -1
  129. package/dist/setup.js +223 -6
  130. package/dist/setup.js.map +1 -1
  131. package/dist/sign.d.ts.map +1 -1
  132. package/dist/sign.js +18 -1
  133. package/dist/sign.js.map +1 -1
  134. package/dist/skills-verify.d.ts +34 -1
  135. package/dist/skills-verify.d.ts.map +1 -1
  136. package/dist/skills-verify.js +82 -0
  137. package/dist/skills-verify.js.map +1 -1
  138. package/dist/stage-line.d.ts +68 -0
  139. package/dist/stage-line.d.ts.map +1 -0
  140. package/dist/stage-line.js +129 -0
  141. package/dist/stage-line.js.map +1 -0
  142. package/dist/statusline.d.ts +99 -0
  143. package/dist/statusline.d.ts.map +1 -1
  144. package/dist/statusline.js +310 -44
  145. package/dist/statusline.js.map +1 -1
  146. package/dist/store-counts.d.ts +26 -0
  147. package/dist/store-counts.d.ts.map +1 -0
  148. package/dist/store-counts.js +125 -0
  149. package/dist/store-counts.js.map +1 -0
  150. package/dist/store-guard.d.ts +106 -0
  151. package/dist/store-guard.d.ts.map +1 -0
  152. package/dist/store-guard.js +294 -0
  153. package/dist/store-guard.js.map +1 -0
  154. package/dist/swarm-brief.d.ts +95 -0
  155. package/dist/swarm-brief.d.ts.map +1 -0
  156. package/dist/swarm-brief.js +660 -0
  157. package/dist/swarm-brief.js.map +1 -0
  158. package/dist/trace-bundle.d.ts +8 -0
  159. package/dist/trace-bundle.d.ts.map +1 -1
  160. package/dist/trace-bundle.js +11 -0
  161. package/dist/trace-bundle.js.map +1 -1
  162. package/package.json +12 -11
  163. package/sbom.json +804 -154
  164. package/src/agents-policy.ts +46 -2
  165. package/src/amendment-trace.ts +441 -19
  166. package/src/backlog-public.ts +503 -0
  167. package/src/backlog-transitions.ts +77 -0
  168. package/src/backlog.ts +12 -0
  169. package/src/claim-check.ts +25 -2
  170. package/src/claude-hooks-assets.ts +227 -0
  171. package/src/clean-room-smoke.ts +195 -0
  172. package/src/cmd-usage.ts +29 -5
  173. package/src/codex-hooks-assets.ts +140 -30
  174. package/src/codex-hooks.ts +21 -1
  175. package/src/course-staleness.ts +125 -0
  176. package/src/destructive-guard-hook.ts +151 -0
  177. package/src/destructive-guard.ts +3027 -0
  178. package/src/discrimination-gate.ts +98 -19
  179. package/src/feature-adr-routing.ts +220 -22
  180. package/src/guard.ts +318 -0
  181. package/src/harness-core-location.ts +44 -0
  182. package/src/index.ts +111 -3
  183. package/src/lead-shift-gate.ts +145 -0
  184. package/src/ledger-backfill.ts +20 -1
  185. package/src/loop-blobs.generated.ts +12 -3
  186. package/src/loop-lint.ts +52 -7
  187. package/src/loop-plan-graph.ts +66 -1
  188. package/src/loop-trace.ts +13 -1
  189. package/src/managed-hooks.ts +5 -6
  190. package/src/model-recommender.ts +14 -3
  191. package/src/operations.ts +75 -0
  192. package/src/patterns.ts +33 -0
  193. package/src/publish.ts +27 -2
  194. package/src/registry.ts +3 -2
  195. package/src/score.ts +16 -4
  196. package/src/session-retro.ts +466 -11
  197. package/src/setup.ts +246 -9
  198. package/src/sign.ts +18 -1
  199. package/src/skills-verify.ts +99 -1
  200. package/src/stage-line.ts +151 -0
  201. package/src/statusline.ts +396 -47
  202. package/src/store-counts.ts +154 -0
  203. package/src/store-guard.ts +388 -0
  204. package/src/swarm-brief.ts +661 -0
  205. package/src/trace-bundle.ts +10 -0
package/src/operations.ts CHANGED
@@ -169,6 +169,26 @@ export interface InitReport {
169
169
  // Platform enrichment — optional extras beyond SKILL.md
170
170
  // ---------------------------------------------------------------------------
171
171
 
172
+ /**
173
+ * Размер RVF-файла, при котором в нём заведомо НЕТ ни одной записи.
174
+ *
175
+ * ИЗМЕРЕНО 2026-09-04 прямым прогоном на `@ruvector/rvf-node` 0.1.8 (linux-x64): пустое хранилище —
176
+ * РОВНО 162 байта при любой размерности; первая запись доводит файл до 615 байт при размерности 8,
177
+ * 839 при 64, 2119 при 384 и 6727 при 1536. Порог 200 — небольшой запас над измеренным пустым
178
+ * размером: он покрывает возможный чуть больший заголовок и втрое ниже самого маленького
179
+ * наблюдавшегося хранилища С записью.
180
+ *
181
+ * ПОПРАВКА К ПЕРВОЙ РЕДАКЦИИ (ревью 2026-09-04). Здесь стояло «первая запись добавляет ~1550 байт»
182
+ * и «порог — середина измеренного зазора». Оба утверждения неверны: 1550 нигде не измерялось, а
183
+ * серединой зазора [162, 2119] было бы 1140, не 200. Строгое на вид обоснование опиралось на
184
+ * арифметическую ошибку в собственных числах документа.
185
+ *
186
+ * Оговорка «при меньшей размерности запись могла бы уместиться в зазор» тоже СНЯТА как
187
+ * неизмеренная: замер по четырём размерностям показывает обратное — даже при размерности 8
188
+ * хранилище с записью втрое больше порога.
189
+ */
190
+ const RVF_HEADER_ONLY_MAX_BYTES = 200;
191
+
172
192
  /**
173
193
  * Where each target's skill tree is rooted. These MUST agree with the
174
194
  * `skillsRoot` each adapter actually emits to — so we import the adapters'
@@ -1187,6 +1207,61 @@ export async function runDoctor(options: { projectRoot: string }): Promise<Docto
1187
1207
  detail: 'orphan .dz/memory.rvf placeholder from an old setup (nothing reads it) — delete it; the real store is .dz/agentdb.db',
1188
1208
  });
1189
1209
  }
1210
+ // 8y. ПУСТОЕ ВЕКТОРНОЕ ХРАНИЛИЩЕ МОЛЧИТ, А НЕ ЖАЛУЕТСЯ (решение владельца 2026-09-04).
1211
+ //
1212
+ // Пустой RVF-файл не даёт ошибки: поиск по нему возвращает пустой список, и всё выглядит
1213
+ // работающим. ИЗМЕРЕНО 2026-09-04: `.agentic-qe/patterns.rvf` — 162 байта (записей не помещается)
1214
+ // при 288 паттернах и 275 векторах, целых в `.agentic-qe/memory.db` рядом. Подробности,
1215
+ // измерения и названные пределы — features/doctor-empty-vector-store/01_requirements.md.
1216
+ //
1217
+ // ПОЧЕМУ ПО РАЗМЕРУ, А НЕ ПО ЧИСЛУ ВЕКТОРОВ. Единственная причина — не потревожить наблюдение:
1218
+ // идёт семидневный замер «читает ли этот файл хоть кто-нибудь», а открытие сдвигает время
1219
+ // последнего чтения. Прежняя редакция называла второй причиной файловый замок — ЭТО ОПРОВЕРГНУТО
1220
+ // ревью экспериментально: `openReadonly` под живым замком открывает хранилище, не создаёт `.lock`
1221
+ // и не меняет ни размер, ни mtime. Ложное обоснование убрано, верное оставлено.
1222
+ //
1223
+ // ЧТО ДОКАЗЫВАЕТ РАЗМЕР: ровно «записей тут поместиться не может». НЕ доказывает, что файл —
1224
+ // валидный заголовок формата: пустой, обрезанный или посторонний файл с тем же расширением
1225
+ // попадёт в ту же ветку, поэтому текст находки говорит про размер и следствие.
1226
+ //
1227
+ // ОБЛАСТЬ — ТРИ ИЗВЕСТНЫХ КАТАЛОГА ХРАНИЛИЩ, без рекурсии. `.dz/memory` добавлен по находке
1228
+ // ревью: собственное RVF-хранилище dz лежит именно там (vector-tier.ts), и без него ветка `.dz`
1229
+ // покрывала только legacy-заглушку, то есть была декоративной. Рекурсия по дереву не делается:
1230
+ // она нашла бы десятки копий в `.claude/worktrees/agent-*/`, и отчёт утонул бы в шуме.
1231
+ for (const dir of ['.agentic-qe', '.dz', '.dz/memory']) {
1232
+ let names: string[] = [];
1233
+ try {
1234
+ names = readdirSync(join(root, dir)).filter((n) => n.endsWith('.rvf'));
1235
+ } catch { continue; } // каталога нет — проверять нечего, и это не находка
1236
+ for (const name of names) {
1237
+ // `.dz/memory.rvf` уже разобран проверкой выше как legacy-сирота с советом «удалить».
1238
+ // Вторая находка на тот же файл советовала бы искать держателя у файла, который никто не
1239
+ // читает — два совета об одном объекте, и один из них бессмысленный (находка ревью).
1240
+ if (dir === '.dz' && name === 'memory.rvf') continue;
1241
+ let size: number;
1242
+ try {
1243
+ const st = statSync(join(root, dir, name));
1244
+ // Каталог с таким именем — не хранилище; `isFile` дешевле и честнее, чем гадать по размеру.
1245
+ if (!st.isFile()) continue;
1246
+ size = st.size;
1247
+ } catch { continue; } // нечитаемый или исчезнувший файл — не находка: мы про него ничего не установили
1248
+ if (size > RVF_HEADER_ONLY_MAX_BYTES) continue;
1249
+ // СООБЩЕНИЕ, А НЕ ПРОВАЛ. Проверка не умеет отличить испорченное хранилище от только что
1250
+ // созданного, в которое ещё ничего не писали, — а это здоровое состояние первого дня. Ронять
1251
+ // код возврата на здоровом состоянии значит приучить читателя к красному; соседняя проверка
1252
+ // целостности в этой же функции решает ту же дилемму так же и говорит об этом вслух.
1253
+ checks.push({
1254
+ name: 'vector store',
1255
+ ok: true,
1256
+ detail: `${dir}/${name} — ${size} bytes: too small to hold a single record, so a search `
1257
+ + 'over it returns an empty answer instead of an error, and nothing looks broken. '
1258
+ + 'Size alone does not say WHY: an empty store, a truncated file, or not a store at all. '
1259
+ + 'If this project HAS learned patterns, the store is behind — compare with the SQLite '
1260
+ + 'store beside it before deciding.',
1261
+ });
1262
+ }
1263
+ }
1264
+
1190
1265
  const legacyMcp = join(root, '.claude', 'mcp.json');
1191
1266
  if (existsSync(legacyMcp) && readFileSync(legacyMcp, 'utf-8').includes('agentdb')) {
1192
1267
  checks.push({
package/src/patterns.ts CHANGED
@@ -60,6 +60,29 @@ export interface PatternRecord {
60
60
  readonly lessonPairId?: string;
61
61
  /** Read-model companion; derived from the linked class row, never persisted on this row. */
62
62
  readonly classForm?: string;
63
+ /**
64
+ * Address of this record IN THE STORE IT CAME FROM — `MemoryRecord.id`, carried so that
65
+ * `dz recall --forget` / `--promote` can be driven from `dz recall --all --json`, which their own
66
+ * help text already tells the reader to use. Deriving it instead is the trap `cmdRecallForget`
67
+ * already warns about: MEASURED on this store, `patternRecordId(recordToPattern(r))` reproduces
68
+ * the id for 535 of 537 records and hands a plausible WRONG address for the other two (a
69
+ * `dream:`-written row and one on an older scheme) — and a wrong id makes `--forget` report
70
+ * success while deleting nothing. So the id travels rather than being recomputed.
71
+ *
72
+ * VALID ONLY IN THE ISSUING STORE. `dz teach --from-json` must never adopt it: an id names a
73
+ * DIFFERENT record on the receiving machine, so an adopted id would point `--forget` at the wrong
74
+ * lesson. The importer builds its candidate from named fields (never a spread), and a test pins
75
+ * that — the property is what keeps the portable export safe to accept from anyone.
76
+ */
77
+ readonly dzId?: string;
78
+ /**
79
+ * Quarantine marker (feature lesson-quarantine): the lesson is a HYPOTHESIS, not knowledge.
80
+ * Present ONLY when true — absence means promoted, matching `readQuarantineState`'s AM-1
81
+ * fail-safe, so a missing field is never read as a claim about the record. Carried because
82
+ * `--promote` addresses exactly the quarantined rows and had no way to name them from an export.
83
+ * Like `dzId`, it is a fact about the ISSUING store and is not adopted on import.
84
+ */
85
+ readonly quarantined?: boolean;
63
86
  }
64
87
 
65
88
  /** A session lifecycle event as written by the session hooks to `.dz/sessions.jsonl`. */
@@ -498,6 +521,15 @@ export function recordToPattern(r: MemoryRecord): PatternRecord {
498
521
  && typeof lessonPairId === 'string' && lessonPairId !== ''
499
522
  ? { lessonForm, lessonPairId }
500
523
  : {};
524
+ // The id and the quarantine marker travel with the row. Both are facts about THIS store, and
525
+ // both were previously dropped here — the single place the whole `--all --json` export is built
526
+ // (`loadStorePatternsSync` = `loadStoreRecordsSync().map(recordToPattern)`), which is why
527
+ // `--forget`'s own "find them with `dz recall --all --json`" hint pointed at a file that could
528
+ // not answer. `quarantined` is emitted only when true, so absence keeps meaning "promoted".
529
+ const addressing: { dzId?: string; quarantined?: boolean } = {
530
+ ...(typeof r.id === 'string' && r.id !== '' ? { dzId: r.id } : {}),
531
+ ...(readQuarantineState(r).quarantined ? { quarantined: true } : {}),
532
+ };
501
533
  return {
502
534
  pattern: r.text,
503
535
  type: PATTERN_TYPES.has(r.outcome) ? (r.outcome as PatternRecord['type']) : 'lesson-learned',
@@ -506,6 +538,7 @@ export function recordToPattern(r: MemoryRecord): PatternRecord {
506
538
  ts: r.timestamp,
507
539
  source: r.metadata?.['source'] ?? 'dz-teach',
508
540
  ...pair,
541
+ ...addressing,
509
542
  };
510
543
  }
511
544
 
package/src/publish.ts CHANGED
@@ -27,6 +27,17 @@ export interface PublishResult {
27
27
  * unmodified `publishPackages` call is byte-compatible with pre-gate behavior.
28
28
  */
29
29
  readonly claimCheck?: { readonly findings: number; readonly high: number } | undefined;
30
+ /**
31
+ * DRY-RUN ONLY, and the reason it exists is a measured incident. A dry run short-circuits
32
+ * BEFORE build, sign and pack (see the `opts.dryRun` branch below), so the package's own
33
+ * `prepublishOnly` gate never executes. On 2026-09-02 a clean dry run was read as evidence that
34
+ * publication would succeed; the real gate was RED — a stale signature baseline plus six
35
+ * `__pycache__/*.pyc` files already signed into the manifest. A preview that names only what it
36
+ * DID check reads as a pass for everything it skipped, which is the same failure class as a gate
37
+ * that infers success from silence. So a dry-run result carries the list of gates it did NOT run,
38
+ * and the CLI prints it. Absent on a real publish, where every gate actually ran.
39
+ */
40
+ readonly notVerified?: readonly string[] | undefined;
30
41
  }
31
42
 
32
43
  /** Full publish report. */
@@ -630,8 +641,22 @@ export function publishPackages(
630
641
  }
631
642
 
632
643
  if (opts.dryRun) {
633
- results.push({ name: pkg.name, oldVersion, newVersion, status: 'skipped', claimCheck: claimCheckSummary });
634
- landedInBatch.add(pkg.name); // preview: this package passed its gates and WOULD land
644
+ // NOT a statement that the package would publish cleanly — only that the gates checked ABOVE
645
+ // this line passed. Everything below it (build, re-sign, pack, the package's own
646
+ // `prepublishOnly`, the registry itself) is untouched by a dry run and is named as such.
647
+ const NOT_VERIFIED_BY_DRY_RUN = Object.freeze([
648
+ 'prepublishOnly пакета (его собственный гейт публикации)',
649
+ 'сборка dist из исходников',
650
+ 'пере-подпись манифеста после бампа',
651
+ 'содержимое тарбола (npm pack)',
652
+ 'ответ реестра npm',
653
+ ]);
654
+ results.push({
655
+ name: pkg.name, oldVersion, newVersion, status: 'skipped',
656
+ claimCheck: claimCheckSummary,
657
+ notVerified: NOT_VERIFIED_BY_DRY_RUN,
658
+ });
659
+ landedInBatch.add(pkg.name);
635
660
  continue;
636
661
  }
637
662
 
package/src/registry.ts CHANGED
@@ -363,8 +363,9 @@ function categoryFromPack(pack: string): string {
363
363
  // digitizer outputs like `skills-12factor` (The Twelve-Factor App). Future digitized public packs
364
364
  // (e.g. AOSA) should be added here or carry a `book`/digitized marker.
365
365
  if (pack.includes('book') || pack.includes('12factor')) return 'knowledge';
366
- // Course/tutorial manufacturing (skills-tutorial-factory: package Head-First edu-site course).
367
- if (pack.includes('tutorial')) return 'learning';
366
+ // Learning and demo-site manufacturing: tutorial packs teach; the demo publisher records a
367
+ // product journey and emits a Pages-ready explanatory site.
368
+ if (pack.includes('tutorial') || pack.includes('demo-publisher')) return 'learning';
368
369
  // Packs that carry skills without the `skills-` prefix, catalogued since 2026-09-01. Each needs a
369
370
  // category or it lands in `other`, and a test rightly forbids that bucket: an uncategorised skill
370
371
  // cannot be filtered for, which is half of being findable.
package/src/score.ts CHANGED
@@ -20,6 +20,8 @@
20
20
  * PURE: the CLI reads the artifact files; this module only classifies.
21
21
  */
22
22
 
23
+ import { amendmentIdsIn } from './amendment-trace.js';
24
+
23
25
  export type DisciplineVerdict = 'pass' | 'partial' | 'absent';
24
26
 
25
27
  export interface DisciplineScore {
@@ -524,11 +526,21 @@ export function scoreRun(slug: string, artifacts: RunArtifacts): RunScorecard {
524
526
  // 7. Amendment confirmation — only when amendments EXIST; their absence is not a failure.
525
527
  // Born in the PLAN/ideation: a stray "AM-7" in QE prose (e.g. another feature's test name)
526
528
  // must not conjure the discipline (caught by the very first dogfood run).
527
- const plannedAm = [...new Set(planText.match(/AM-\d+/g) ?? [])].sort();
529
+ // ONE reader of the `AM-*` token, shared with the row grammar and pinned against K2's by
530
+ // test/amendment-grammar-agreement.test.ts. It used to be a THIRD private contract here —
531
+ // `/AM-\d+/g`, which does not match `AM-CP-1` at all (MEASURED 2026-09-06:
532
+ // `'AM-CP-1'.match(/AM-\d+/g)` → `null`), so a plan whose amendments were all challenge-panel
533
+ // rows scored `plannedAm = []` and this whole discipline was skipped silently — no `absent`, no
534
+ // `partial`, just a smaller denominator. A check that silently checks nothing is the class the
535
+ // amendment gate exists to remove, and it had reappeared in the scorer.
536
+ const plannedAm = amendmentIdsIn(planText).sort();
528
537
  if (plannedAm.length > 0) {
529
- // One stray AM-9 in QE must not satisfy AM-1..AM-2 (Codex QE #3): coverage is id-by-id.
530
- const covered = plannedAm.filter((id) => qeText.includes(id));
531
- const missing = plannedAm.filter((id) => !qeText.includes(id));
538
+ // One stray AM-9 in QE must not satisfy AM-1..AM-2 (Codex QE #3): coverage is id-by-id — and
539
+ // `includes` was not id-by-id: a planned `AM-1` read as covered by a report mentioning only
540
+ // `AM-10` (MEASURED — `'AM-10'.includes('AM-1')` `true`). A substring is not an id.
541
+ const mentionedInQe = new Set(amendmentIdsIn(qeText));
542
+ const covered = plannedAm.filter((id) => mentionedInQe.has(id));
543
+ const missing = plannedAm.filter((id) => !mentionedInQe.has(id));
532
544
  add(
533
545
  'amendment-confirmation',
534
546
  'amendments carried into QE with confirmation',