geneseed 3.8.0 → 3.10.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/CHANGELOG.md +71 -1
- package/DESIGN.md +13 -10
- package/QUICKSTART.md +1 -1
- package/README.md +31 -32
- package/SETUP.md +7 -42
- package/SHIPPED.md +4 -4
- package/adapters/bob/README.md +17 -1
- package/adapters/openclaude/README.md +1 -1
- package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +1 -1
- package/adapters/opencode/README.md +3 -3
- package/adapters/opencode/plugins/geneseed-context.js +78 -8
- package/adapters/opencode/plugins/geneseed-guard.js +9 -9
- package/adapters/opencode/plugins/geneseed-workflow.js +1 -1
- package/adapters/opencode/workflows/_runtime.js +2 -2
- package/adapters/opencode/workflows/dispatch.js +1 -1
- package/bin/geneseed-hook.mjs +1 -1
- package/docs/candidate-hosts.md +12 -5
- package/docs/extending.md +72 -40
- package/docs/token-footprint.md +26 -21
- package/docs/web/claude-hooks.md +1 -1
- package/docs/web/collaboration.md +1 -1
- package/docs/web/exclusions.md +0 -1
- package/docs/web/footprint.md +1 -1
- package/docs/web/harnesses.md +14 -14
- package/docs/web/install-quick.md +1 -1
- package/docs/web/plugin-guard.md +2 -2
- package/docs/web/rules.md +1 -1
- package/docs/web/themes-author.md +2 -2
- package/docs/web-ui.md +2 -2
- package/harness.config.json +1 -1
- package/js/README.md +8 -7
- package/js/build/driver.mjs +26 -95
- package/js/build/emit-claude.mjs +76 -81
- package/js/build/generate.mjs +0 -1
- package/js/build/render.mjs +10 -3
- package/js/build/source.mjs +105 -1
- package/js/build/stubs.mjs +25 -4
- package/js/cli-table.json +8 -8
- package/js/hosts/hooks.mjs +91 -59
- package/js/hosts/hosts.mjs +5 -21
- package/js/hosts/installs.mjs +4 -6
- package/js/hosts/mcp.mjs +3 -13
- package/js/hosts/native.mjs +28 -51
- package/js/hosts/opencode.mjs +1 -1
- package/js/hosts/settings.mjs +4 -78
- package/js/inspect/checks-authoring.mjs +153 -117
- package/js/inspect/checks-repo.mjs +45 -1
- package/js/inspect/diff.mjs +11 -3
- package/js/inspect/doctor.mjs +3 -3
- package/js/inspect/excludes.mjs +0 -4
- package/js/inspect/inventory.mjs +1 -2
- package/js/inspect/status.mjs +4 -4
- package/js/maintain/migrate.mjs +5 -5
- package/js/maintain/setup.mjs +3 -5
- package/js/maintain/uninstall.mjs +8 -42
- package/js/ui/tui.mjs +1 -1
- package/js/web/actions.mjs +4 -5
- package/js/web/api.mjs +10 -5
- package/package.json +1 -2
- package/registry.json +7 -0
- package/src/AGENT.md.tmpl +86 -65
- package/src/agents/_template.md +3 -3
- package/src/agents/advocate.md +3 -3
- package/src/agents/architect.md +3 -3
- package/src/agents/developer.md +4 -4
- package/src/agents/docs.md +3 -3
- package/src/agents/empiricist.md +4 -4
- package/src/agents/explorer.md +4 -4
- package/src/agents/framer.md +3 -3
- package/src/agents/historian.md +3 -3
- package/src/agents/operator.md +3 -3
- package/src/agents/pragmatist.md +3 -3
- package/src/agents/researcher.md +7 -7
- package/src/agents/reviewer.md +3 -3
- package/src/agents/security.md +3 -3
- package/src/agents/skeptic.md +3 -3
- package/src/agents/steward.md +3 -3
- package/src/agents/tester.md +3 -3
- package/src/agents/user-advocate.md +3 -3
- package/src/agents/visionary.md +3 -3
- package/src/doctrines/README.md +4 -3
- package/src/doctrines/comms.md +4 -4
- package/src/doctrines/craft.md +20 -20
- package/src/doctrines/ops.md +19 -19
- package/src/doctrines/process.md +20 -20
- package/src/doctrines/rigor.md +22 -22
- package/src/laws/universal.md +26 -44
- package/src/memory/README.md +3 -3
- package/src/modes/foreman.md +1 -1
- package/src/notebook/README.md +1 -1
- package/src/ontology/universal.md +8 -10
- package/src/postures/artisan.md +2 -2
- package/src/skills/_self-improvement.md +4 -4
- package/src/skills/_template.md +1 -1
- package/src/skills/brainstorm.md +3 -3
- package/src/skills/bruno-collection-generator.md +6 -6
- package/src/skills/bruno-test-writer.md +5 -5
- package/src/skills/ci-fix.md +4 -4
- package/src/skills/commit.md +11 -10
- package/src/skills/consolidate-memory.md +4 -4
- package/src/skills/council.md +2 -2
- package/src/skills/daydream/SKILL.md +10 -3
- package/src/skills/daydream/VENDOR.md +10 -2
- package/src/skills/daydream/instructions.md +2 -2
- package/src/skills/debug.md +4 -4
- package/src/skills/deps-audit.md +3 -3
- package/src/skills/develop.md +7 -7
- package/src/skills/document-project.md +6 -6
- package/src/skills/explain-changes/SKILL.md +69 -0
- package/src/skills/explain-changes/VENDOR.md +20 -0
- package/src/skills/explain-changes/scripts/render_changes.mjs +558 -0
- package/src/skills/forge-mcp.md +4 -4
- package/src/skills/fresh-eyes.md +4 -4
- package/src/skills/frontend-design.md +1 -1
- package/src/skills/geneseed.md +4 -4
- package/src/skills/git-archaeology.md +5 -5
- package/src/skills/git-rescue.md +7 -7
- package/src/skills/handoff.md +3 -3
- package/src/skills/ingest.md +5 -5
- package/src/skills/migrate.md +8 -8
- package/src/skills/parallel-agents.md +2 -2
- package/src/skills/pipeline.md +2 -2
- package/src/skills/plan.md +3 -3
- package/src/skills/ponytail.md +2 -2
- package/src/skills/profile.md +2 -2
- package/src/skills/prose.md +1 -1
- package/src/skills/prototype.md +1 -1
- package/src/skills/react-view-transitions/SKILL.md +1 -1
- package/src/skills/react-view-transitions/VENDOR.md +7 -2
- package/src/skills/refactor.md +3 -3
- package/src/skills/release.md +5 -5
- package/src/skills/repo-map.md +4 -4
- package/src/skills/research.md +9 -9
- package/src/skills/review-response.md +4 -4
- package/src/skills/rule.md +3 -3
- package/src/skills/security-audit.md +5 -5
- package/src/skills/ship.md +5 -5
- package/src/skills/skill-forge.md +4 -4
- package/src/skills/token-report/SKILL.md +2 -3
- package/src/skills/token-report/VENDOR.md +2 -3
- package/src/skills/token-report/scripts/token_report.mjs +7 -121
- package/src/skills/wiki.md +5 -5
- package/src/skills/workflow.md +3 -4
- package/src/skills/worktree.md +4 -4
- package/themes/_TEMPLATE.json +37 -39
- package/themes/biker.json +37 -39
- package/themes/commentator.json +37 -39
- package/themes/cyberpunk.json +37 -39
- package/themes/gamer.json +37 -39
- package/themes/imperial.json +37 -39
- package/themes/joker.json +37 -39
- package/themes/marvin.json +37 -39
- package/themes/mean.json +37 -39
- package/themes/military.json +37 -39
- package/themes/neutral.json +37 -39
- package/themes/pirate.json +37 -39
- package/themes/sports.json +37 -39
- package/themes/verstappen.json +37 -39
- package/themes/wizard.json +37 -39
- package/web/dist/assets/Activity-BjVp522q.js +1 -0
- package/web/dist/assets/ActivityDetail-8ep1bspW.js +1 -0
- package/web/dist/assets/Dashboard-OkC_PCH6.js +2 -0
- package/web/dist/assets/{Docs-DrJNDPwI.js → Docs-CKPWz00o.js} +2 -2
- package/web/dist/assets/FilterInput-CVyqmYWM.js +1 -0
- package/web/dist/assets/GreenhouseView-D0cbCJ5m.js +2 -0
- package/web/dist/assets/Installs-BtGGdoTt.js +2 -0
- package/web/dist/assets/JournalView-Ckl4dmzR.js +1 -0
- package/web/dist/assets/Laws-CLpdRRk8.js +3 -0
- package/web/dist/assets/Library-wPPuKQrI.js +1 -0
- package/web/dist/assets/{Markdown-BQAvN8xx.js → Markdown-CkFyfgNf.js} +14 -14
- package/web/dist/assets/OperatorHudView-CiAkAkQL.js +2 -0
- package/web/dist/assets/Personal-D8CnDo9v.js +1 -0
- package/web/dist/assets/Tabs-B4HNrrCs.js +1 -0
- package/web/dist/assets/drawable-BiwaOo6x.js +1 -0
- package/web/dist/assets/geist-mono-latin-400-normal-DTRLJnHl.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-500-normal-YINYabwD.woff2 +0 -0
- package/web/dist/assets/index-C1yil5j1.js +2 -0
- package/web/dist/assets/index-CDPDk2vj.css +1 -0
- package/web/dist/assets/{react-DbkvxWG5.js → react-B9Z_tXRV.js} +1 -1
- package/web/dist/assets/sections-GalMS0Zv.js +1 -0
- package/web/dist/assets/usePoll-O0sJEw1Y.js +1 -0
- package/web/dist/index.html +4 -4
- package/web/src/pages/Laws.jsx +616 -361
- package/adapters/copilot/README.md +0 -105
- package/web/dist/assets/Activity-CH-7cPIM.js +0 -1
- package/web/dist/assets/ActivityDetail-DZvz0xS5.js +0 -1
- package/web/dist/assets/CatalogRow-DftXpZHY.js +0 -1
- package/web/dist/assets/Diff-DvwdG0L9.js +0 -2
- package/web/dist/assets/Doctor-OTmwmYZg.js +0 -1
- package/web/dist/assets/FilterInput-BakJLvaV.js +0 -1
- package/web/dist/assets/GreenhouseView-XxrAdriX.js +0 -2
- package/web/dist/assets/Harness-C_LYNk5c.js +0 -1
- package/web/dist/assets/JournalView-BcuyeEaR.js +0 -1
- package/web/dist/assets/Laws-CWHrTKY4.js +0 -3
- package/web/dist/assets/Library-CA9IHghn.js +0 -1
- package/web/dist/assets/OperatorHudView-BMRY1Tt4.js +0 -2
- package/web/dist/assets/Profile-wceET1JY.js +0 -1
- package/web/dist/assets/Rules-BEXvY4Tx.js +0 -1
- package/web/dist/assets/Settings-DS2zF0Ez.js +0 -1
- package/web/dist/assets/Skills-DahlOJuI.js +0 -1
- package/web/dist/assets/StatusBadge-Cg6j-NJL.js +0 -1
- package/web/dist/assets/activity-iWaWVtsR.js +0 -1
- package/web/dist/assets/audiowide-latin-400-normal-6GFCX7ni.woff2 +0 -0
- package/web/dist/assets/drawable-BTHjTzF7.js +0 -1
- package/web/dist/assets/geist-mono-latin-400-normal-DKaoCDn5.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-500-normal-C3sF8Y1B.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-600-normal-BHzjB6_C.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-400-normal-BIGslYFI.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-500-normal-REKAfBVG.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-600-normal-DAxitmSu.woff2 +0 -0
- package/web/dist/assets/index-AN5Emnrg.js +0 -2
- package/web/dist/assets/index-Ruh93A3A.css +0 -1
- package/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/web/dist/assets/lawCats-Btjp3Hk_.js +0 -1
- package/web/dist/assets/oswald-latin-400-normal-BAFo8sP8.woff2 +0 -0
- package/web/dist/assets/oswald-latin-500-normal-B-eyT0Dw.woff2 +0 -0
- package/web/dist/assets/oswald-latin-600-normal-plFmSr5g.woff2 +0 -0
- package/web/dist/assets/oswald-latin-700-normal-vy6hmKSr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/web/dist/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/web/dist/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/web/dist/assets/rolldown-runtime-CbXtAM7H.js +0 -1
package/js/hosts/settings.mjs
CHANGED
|
@@ -649,7 +649,7 @@ export function hookPrefix({ runner, entry, platform = process.platform } = {})
|
|
|
649
649
|
* rule is no longer BINDING — so a gate that stopped every commit to ask for consent to a rule
|
|
650
650
|
* the install did not adopt is the disagreement the three-tier split forbids. The group stays
|
|
651
651
|
* and gains `--no-consent`, which skips only the process-5 branch. It once DROPPED the whole
|
|
652
|
-
* group instead, and Law IV went with it — on Claude only, since Bob
|
|
652
|
+
* group instead, and Law IV went with it — on Claude only, since Bob carries it
|
|
653
653
|
* through `tool-gate`. The command changing is also what re-wires an existing install:
|
|
654
654
|
* `mergeClaudeSettings` prunes every previously-managed group that is not in this return
|
|
655
655
|
* value.
|
|
@@ -700,22 +700,6 @@ export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = [],
|
|
|
700
700
|
Stop: [{ hooks: [{ type: 'command', command: `${run} learn ${mem} || exit 0` }] }],
|
|
701
701
|
};
|
|
702
702
|
}
|
|
703
|
-
if (host === 'copilot') {
|
|
704
|
-
// COPILOT'S OWN SCHEMA (docs.github.com/en/copilot/reference/hooks-configuration): camelCase
|
|
705
|
-
// events, each an ARRAY of `{type: "command", command}` entries — no matcher groups, so the
|
|
706
|
-
// array elements ARE the groups this merge appends and prunes. `preToolUse` answers with
|
|
707
|
-
// a top-level `permissionDecision` (`--host copilot`), and `agentStop`/`preCompact` carry
|
|
708
|
-
// `transcriptPath`, so `learn` runs here as on Claude. No matcher, so `tool-gate` reads
|
|
709
|
-
// the payload's shape. Unverified live: no Copilot CLI on the authoring machine.
|
|
710
|
-
const c = ' --host copilot';
|
|
711
|
-
const entry = (command) => [{ type: 'command', command }];
|
|
712
|
-
return {
|
|
713
|
-
sessionStart: entry(`${run} context --root "${cfg}"${c} || exit 0`),
|
|
714
|
-
preToolUse: entry(`${run} tool-gate --root "${cfg}"${c}`),
|
|
715
|
-
agentStop: entry(`${run} learn ${mem} || exit 0`),
|
|
716
|
-
preCompact: entry(`${run} learn ${mem} || exit 0`),
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
703
|
// OpenClaude takes Claude's whole group set and verdicts unchanged; only `context` needs
|
|
720
704
|
// the host, to know which root file OpenClaude already loads by itself.
|
|
721
705
|
const h = host === 'openclaude' ? ' --host openclaude' : '';
|
|
@@ -753,63 +737,6 @@ export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = [],
|
|
|
753
737
|
};
|
|
754
738
|
}
|
|
755
739
|
|
|
756
|
-
/**
|
|
757
|
-
* LEGACY: the single-slot Copilot shape (`hooks.<event>` = ONE object, events `sessionStart` /
|
|
758
|
-
* `toolCall`) an emit wrote before Copilot's hooks became event → array. Copilot has no
|
|
759
|
-
* `toolCall` event any more, so those gates never ran. Nothing writes this shape now; these
|
|
760
|
-
* two only take an older install's recorded `copilot_hooks` claims back out.
|
|
761
|
-
*/
|
|
762
|
-
/** Remove exactly the recorded Copilot hooks from `p`. True when the file was rewritten. */
|
|
763
|
-
export function unwireCopilotSettings(p, recorded) {
|
|
764
|
-
if (!existsSync(p) || !recorded || !recorded.length) return false;
|
|
765
|
-
let loaded;
|
|
766
|
-
let hadComments;
|
|
767
|
-
try {
|
|
768
|
-
[loaded, hadComments] = readJsonc(readText(p));
|
|
769
|
-
} catch (e) {
|
|
770
|
-
if (!isOsError(e)) throw e;
|
|
771
|
-
return false;
|
|
772
|
-
}
|
|
773
|
-
if (hadComments || !isDict(loaded)) return false;
|
|
774
|
-
const hooks = get(loaded, 'hooks');
|
|
775
|
-
if (!isDict(hooks)) return false;
|
|
776
|
-
for (const rec of recorded) {
|
|
777
|
-
const event = get(rec, 'event');
|
|
778
|
-
if (deepEquals(get(hooks, event), get(rec, 'hook'))) delete hooks[event];
|
|
779
|
-
}
|
|
780
|
-
if (!Object.keys(hooks).length) delete loaded.hooks;
|
|
781
|
-
try {
|
|
782
|
-
atomicWriteJson(p, loaded);
|
|
783
|
-
} catch (e) {
|
|
784
|
-
if (!isOsError(e)) throw e;
|
|
785
|
-
return false;
|
|
786
|
-
}
|
|
787
|
-
return true;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
/**
|
|
791
|
-
* The Copilot twin of `settingsIntegrityCheck`: every recorded hook present (or absent) in
|
|
792
|
-
* the file it was claimed against. Same contract — problems as strings, each also WARNed on
|
|
793
|
-
* stderr, empty when clean — so a merge that did not stick is loud at emit time rather than
|
|
794
|
-
* a hook that quietly never fires.
|
|
795
|
-
*/
|
|
796
|
-
export function copilotIntegrityCheck(p, recorded, expect = 'present') {
|
|
797
|
-
const problems = [];
|
|
798
|
-
const hooks = existsSync(p) ? (() => {
|
|
799
|
-
try { return get(readJsonc(readText(p))[0], 'hooks'); } catch { return null; }
|
|
800
|
-
})() : null;
|
|
801
|
-
for (const rec of (recorded || []).filter(isDict)) {
|
|
802
|
-
const hit = deepEquals(get(hooks, rec.event), rec.hook);
|
|
803
|
-
if (expect === 'present' && !hit) {
|
|
804
|
-
problems.push(`${p}: recorded Copilot hook missing — event=${rec.event}`);
|
|
805
|
-
} else if (expect === 'absent' && hit) {
|
|
806
|
-
problems.push(`${p}: recorded Copilot hook still present after unwire — event=${rec.event}`);
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
for (const x of problems) process.stderr.write(`[geneseed] WARN: ${x}\n`);
|
|
810
|
-
return problems;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
740
|
/**
|
|
814
741
|
* `_build_settings._merge_claude_settings` — returns `[target, managed]`.
|
|
815
742
|
*
|
|
@@ -1045,8 +972,8 @@ const SHIM_ENTRY_MARK = 'geneseed-hook.mjs';
|
|
|
1045
972
|
*
|
|
1046
973
|
* PURE — and currently reached only END TO END, by the `migrate/a-node-baked-shim-reads-as-current`
|
|
1047
974
|
* cell. No unit corpus drives it directly, which is worth knowing before trusting it.
|
|
1048
|
-
* 'none' is not a fault: a
|
|
1049
|
-
*
|
|
975
|
+
* 'none' is not a fault: a user's own settings.json may carry three hand-written hooks
|
|
976
|
+
* and no Geneseed entry. That must read as
|
|
1050
977
|
* "nothing to migrate" rather than "unrecognised", or `migrate` refuses on the commonest
|
|
1051
978
|
* config on any machine.
|
|
1052
979
|
*/
|
|
@@ -1181,8 +1108,7 @@ export function settingsIntegrityCheck(p, managed, expect = 'present') {
|
|
|
1181
1108
|
for (const [event, group] of presentGroups) {
|
|
1182
1109
|
const key = `${formatRepr(event)}\u0000${jsonDumpsCompact(group, { sortKeys: true })}`;
|
|
1183
1110
|
if (recordedSet.has(key)) continue;
|
|
1184
|
-
|
|
1185
|
-
const cmds = [...(get(group, 'hooks') || []).filter(isDict), group]
|
|
1111
|
+
const cmds = (get(group, 'hooks') || []).filter(isDict)
|
|
1186
1112
|
.map((h) => (has(h, 'command') ? h.command : ''));
|
|
1187
1113
|
if (cmds.some((c) => typeof c === 'string'
|
|
1188
1114
|
&& GENESEED_HOOK_SNIFF.some((mk) => c.includes(mk)))) {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import {
|
|
16
|
-
CONFIG, PACK_ORDER, PLUGIN_SRC, ROOT, SRC, THEMES,
|
|
16
|
+
CONFIG, PACK_ORDER, PLUGIN_SRC, RETIRED_RULE_IDS, ROOT, RULE_ID_RE, SRC, THEMES, canonFiles,
|
|
17
|
+
knownRuleIds, ruleCanon, titleKey,
|
|
17
18
|
} from '../build/source.mjs';
|
|
18
19
|
import { themeFiles } from '../hosts/installs.mjs';
|
|
19
20
|
import { descBlockProblem, firstBlockquote } from '../hosts/native.mjs';
|
|
@@ -21,7 +22,9 @@ import { readText } from '../lib/fs.mjs';
|
|
|
21
22
|
import { parseJson, formatRepr } from '../lib/json.mjs';
|
|
22
23
|
import { which } from '../lib/paths.mjs';
|
|
23
24
|
import { NO_WINDOW } from '../lib/proc.mjs';
|
|
24
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
folderSkillProblems, registryProblems, secretProblems, vendorPinProblems,
|
|
27
|
+
} from './checks-repo.mjs';
|
|
25
28
|
import { LAW_CLASS, LAW_CLASSES, SKILL_CLASS } from './inventory.mjs';
|
|
26
29
|
import { NOTE, globSorted, has, isDir, isFile, rglob, srcStems } from './scan.mjs';
|
|
27
30
|
import { spawnSync } from 'node:child_process';
|
|
@@ -47,7 +50,11 @@ export function romanToInt(num) {
|
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
/**
|
|
50
|
-
* `_harness_build._LAW_META_ROW` — `<arabic>: ['<class>', '<principle>'],` in the web page.
|
|
53
|
+
* `_harness_build._LAW_META_ROW` — `<arabic>: ['<class>', '<principle>', '<id>'],` in the web page.
|
|
54
|
+
*
|
|
55
|
+
* The third field PINS the row to a rule id. The key is the number the catalogue renders, and a
|
|
56
|
+
* number is only a position: remove a law and every later row describes its predecessor's
|
|
57
|
+
* neighbour, with every class still plausible. The pin is what makes that a doctor failure.
|
|
51
58
|
*
|
|
52
59
|
* Either quote style, because a principle carrying an apostrophe is double-quoted. Every
|
|
53
60
|
* `\s*` spans newlines and the trailing comma before `]` is optional, so a row a prettier has
|
|
@@ -57,7 +64,8 @@ export function romanToInt(num) {
|
|
|
57
64
|
* `(?m)` is the `m` flag; JS has no inline form. The `.` inside the captures must NOT match a
|
|
58
65
|
* newline (Python has no `re.S` here), which is JS's default.
|
|
59
66
|
*/
|
|
60
|
-
const LAW_META_ROW =
|
|
67
|
+
const LAW_META_ROW =
|
|
68
|
+
/^[^\S\n]*(\d+):\s*\[\s*(['"])(.*?)\2\s*,\s*(['"])(.*?)\4\s*(?:,\s*(['"])(.*?)\6\s*)?,?\s*\]\s*,?[^\S\n]*$/gm;
|
|
61
69
|
|
|
62
70
|
/**
|
|
63
71
|
* `_harness_build._law_meta_problems` — the web Laws ledger's per-rule copy stays complete.
|
|
@@ -67,7 +75,7 @@ const LAW_META_ROW = /^[^\S\n]*(\d+):\s*\[\s*(['"])(.*?)\2\s*,\s*(['"])(.*?)\4\s
|
|
|
67
75
|
* description and the wrong class chip, while everything upstream is fine. That is exactly
|
|
68
76
|
* how two laws shipped description-less.
|
|
69
77
|
*/
|
|
70
|
-
export function lawMetaProblems(lawNums, lawClass, lawClasses) {
|
|
78
|
+
export function lawMetaProblems(lawNums, lawClass, lawClasses, lawIds = null) {
|
|
71
79
|
const page = path.join(ROOT, 'web', 'src', 'pages', 'Laws.jsx');
|
|
72
80
|
let text;
|
|
73
81
|
try { text = readText(page); } catch (e) {
|
|
@@ -80,11 +88,11 @@ export function lawMetaProblems(lawNums, lawClass, lawClasses) {
|
|
|
80
88
|
}
|
|
81
89
|
const meta = new Map();
|
|
82
90
|
for (const m of block[1].matchAll(LAW_META_ROW)) {
|
|
83
|
-
meta.set(Number(m[1]), [m[3], m[5]]);
|
|
91
|
+
meta.set(Number(m[1]), [m[3], m[5], m[7]]);
|
|
84
92
|
}
|
|
85
93
|
const problems = [];
|
|
86
94
|
const seen = new Set();
|
|
87
|
-
for (const roman of lawNums) {
|
|
95
|
+
for (const [i, roman] of lawNums.entries()) {
|
|
88
96
|
const n = romanToInt(roman);
|
|
89
97
|
seen.add(n);
|
|
90
98
|
if (!meta.has(n)) {
|
|
@@ -93,7 +101,12 @@ export function lawMetaProblems(lawNums, lawClass, lawClasses) {
|
|
|
93
101
|
+ 'render it with a blank Principle');
|
|
94
102
|
continue;
|
|
95
103
|
}
|
|
96
|
-
const [klass, principle] = meta.get(n);
|
|
104
|
+
const [klass, principle, pin] = meta.get(n);
|
|
105
|
+
if (lawIds && pin !== lawIds[i]) {
|
|
106
|
+
problems.push(`[authoring] LAW_META[${n}] is pinned to '${pin ?? '(no id)'}' but rule ${roman} is `
|
|
107
|
+
+ `'${lawIds[i]}' — the rows no longer follow the canon's order, so this principle `
|
|
108
|
+
+ 'describes a different law');
|
|
109
|
+
}
|
|
97
110
|
if (!principle.trim()) {
|
|
98
111
|
problems.push(`[authoring] LAW_META[${n}] has an empty principle line — rule `
|
|
99
112
|
+ `${roman} would render with a blank description`);
|
|
@@ -112,9 +125,9 @@ export function lawMetaProblems(lawNums, lawClass, lawClasses) {
|
|
|
112
125
|
return problems;
|
|
113
126
|
}
|
|
114
127
|
|
|
115
|
-
/** `LAW_META_ROW`'s twin for a STRING key — `'<pack>.<n>': ['<pack>', '<principle>'],`. */
|
|
128
|
+
/** `LAW_META_ROW`'s twin for a STRING key — `'<pack>.<n>': ['<pack>', '<principle>', '<id>'],`. */
|
|
116
129
|
const DOCTRINE_META_ROW =
|
|
117
|
-
/^[^\S\n]*(['"])([a-z]+\.\d+)\1:\s*\[\s*(['"])(.*?)\3\s*,\s*(['"])(.*?)\5\s
|
|
130
|
+
/^[^\S\n]*(['"])([a-z]+\.\d+)\1:\s*\[\s*(['"])(.*?)\3\s*,\s*(['"])(.*?)\5\s*(?:,\s*(['"])(.*?)\7\s*)?,?\s*\]\s*,?[^\S\n]*$/gm;
|
|
118
131
|
|
|
119
132
|
/**
|
|
120
133
|
* `lawMetaProblems`' doctrine half — the console's per-rule Principle column stays complete.
|
|
@@ -130,7 +143,7 @@ const DOCTRINE_META_ROW =
|
|
|
130
143
|
* error a six-way vocabulary cannot: a row pasted from the pack above it, keeping the class of
|
|
131
144
|
* the pack it came from, which would colour the row and label its chip with the wrong pack.
|
|
132
145
|
*/
|
|
133
|
-
export function doctrineMetaProblems(addrs) {
|
|
146
|
+
export function doctrineMetaProblems(addrs, ids = null) {
|
|
134
147
|
const page = path.join(ROOT, 'web', 'src', 'pages', 'Laws.jsx');
|
|
135
148
|
let text;
|
|
136
149
|
try { text = readText(page); } catch (e) {
|
|
@@ -142,17 +155,21 @@ export function doctrineMetaProblems(addrs) {
|
|
|
142
155
|
+ "console's doctrine rows would have no gate on their Principle column"];
|
|
143
156
|
}
|
|
144
157
|
const meta = new Map();
|
|
145
|
-
for (const m of block[1].matchAll(DOCTRINE_META_ROW)) meta.set(m[2], [m[4], m[6]]);
|
|
158
|
+
for (const m of block[1].matchAll(DOCTRINE_META_ROW)) meta.set(m[2], [m[4], m[6], m[8]]);
|
|
146
159
|
const problems = [];
|
|
147
160
|
const seen = new Set();
|
|
148
|
-
for (const addr of addrs) {
|
|
161
|
+
for (const [i, addr] of addrs.entries()) {
|
|
149
162
|
seen.add(addr);
|
|
150
163
|
if (!meta.has(addr)) {
|
|
151
164
|
problems.push(`[authoring] doctrine rule ${addr} has no row in DOCTRINE_META `
|
|
152
165
|
+ '(web/src/pages/Laws.jsx) — the console would render it with a blank Principle');
|
|
153
166
|
continue;
|
|
154
167
|
}
|
|
155
|
-
const [klass, principle] = meta.get(addr);
|
|
168
|
+
const [klass, principle, pin] = meta.get(addr);
|
|
169
|
+
if (ids && pin !== ids[i]) {
|
|
170
|
+
problems.push(`[authoring] DOCTRINE_META['${addr}'] is pinned to '${pin ?? '(no id)'}' but `
|
|
171
|
+
+ `that address is '${ids[i]}' now — the row describes a different rule`);
|
|
172
|
+
}
|
|
156
173
|
if (!principle.trim()) {
|
|
157
174
|
problems.push(`[authoring] DOCTRINE_META['${addr}'] has an empty principle line — that `
|
|
158
175
|
+ 'rule would render with a blank description');
|
|
@@ -267,22 +284,35 @@ export function proseMirrorProblems(readme, web, counts, skillStems, shipped = '
|
|
|
267
284
|
|
|
268
285
|
/** `_build_render._TMPL_SPEC_RE`, as the count gate spells it. */
|
|
269
286
|
const TMPL_SPEC_RE = /\{\{DIR_(AGENTS|SKILLS)\}\}\/([A-Za-z0-9_-]+)\.md/g;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
const
|
|
287
|
+
// A rule is DECLARED by its heading and CITED by its id — see `ruleCanon` in
|
|
288
|
+
// `js/build/source.mjs`. The gates below read the RAW source, because the render resolves ids and
|
|
289
|
+
// a bad one would already be gone from what it hands back.
|
|
290
|
+
const DECL_LINE_RE = /^### \{\{(LAW|DOCTRINE):([^}]*)\}\}[ \t]+\S/;
|
|
291
|
+
const ID_CITE_RE = /\{\{(LAW|DOCTRINE):([^}]*)\}\}/g;
|
|
292
|
+
// The pre-id spellings. Either one in src/ is a citation that names a POSITION, which is exactly
|
|
293
|
+
// what stops being true the next time the canon moves.
|
|
294
|
+
const BARE_CITE_RE = /\{\{LAW\}\}\s+[IVXLCDM]+\b|\{\{DOCTRINE\}\}\s+[a-z]+\s+\d+\b/g;
|
|
295
|
+
// `{{DOCTRINE}}` or `{{DOCTRINE:` and not `DOCTRINE`: `{{DOCTRINES}}` is the SECTION noun and is
|
|
296
|
+
// legal everywhere, including in an invariant that points at the tier as a whole.
|
|
297
|
+
const DOCTRINE_TOKEN_RE = /\{\{DOCTRINE(?:\}\}|:)/;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* The rules the hooks enforce at the tool boundary, and the address each is bound to there. The
|
|
301
|
+
* hook path cannot afford to read the canon on every tool call, so its gate-ledger keys
|
|
302
|
+
* (`law-1`, `law-4`, `process-1`, `process-5` in `js/hosts/hooks.mjs` and the OpenCode guard
|
|
303
|
+
* plugin), `CONSENT_RULE` in `js/hosts/settings.mjs` and the console's `HOOK_GATED` stay
|
|
304
|
+
* number-keyed — and this pin is what turns a renumber that moves one of these rules into a
|
|
305
|
+
* doctor failure instead of a gate silently guarding its neighbour.
|
|
306
|
+
*/
|
|
307
|
+
export const HOOK_PINNED = {
|
|
308
|
+
'sealed-secrets': 'I', 'deletion-is-deliberate': 'IV',
|
|
309
|
+
'persist-insight': 'process 1', 'consent-before-push': 'process 5',
|
|
310
|
+
};
|
|
281
311
|
|
|
282
312
|
/**
|
|
283
313
|
* The LEAN block grammar, and the heading split, both duplicated from `js/build/render.mjs`.
|
|
284
314
|
*
|
|
285
|
-
* Deliberately duplicated
|
|
315
|
+
* Deliberately duplicated rather than imported: both are module-private
|
|
286
316
|
* there, and exporting them so one gate could borrow them would widen the renderer's surface
|
|
287
317
|
* for a reader that only needs to know the shape. Two literals, one paragraph apart from the
|
|
288
318
|
* rule they encode, is cheaper than a seam.
|
|
@@ -377,48 +407,84 @@ export function leanBlockProblems() {
|
|
|
377
407
|
* ⚠ THE VOCABULARY GATES ARE HERE BECAUSE `themeParityProblems` CANNOT SEE THEM. That check is
|
|
378
408
|
* presence-only and SYMMETRIC over the union of every theme's keys: a key missing from all
|
|
379
409
|
* fourteen is in parity, and a key whose VALUE breaks a parser is not its business at all. So
|
|
380
|
-
* `DOC_*` coverage, the `
|
|
410
|
+
* `DOC_*` coverage, the `LEX_*` equality and the single-word tier noun each need a gate
|
|
381
411
|
* that reads the SOURCE and asks what the source requires.
|
|
382
412
|
*/
|
|
383
413
|
export function constitutionProblems() {
|
|
384
414
|
const problems = [];
|
|
385
|
-
const dir = path.join(SRC, 'doctrines');
|
|
386
415
|
|
|
387
|
-
// ----
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
416
|
+
// ---- every rule declares an id, every id is well-formed and unique, none is retired.
|
|
417
|
+
//
|
|
418
|
+
// Numbers are positional now (`ruleCanon`), so contiguity and filing cannot go wrong; what can
|
|
419
|
+
// is the declaration itself. A `### ` line in a canon file that is not `{{KIND:id}} Name`
|
|
420
|
+
// renders nowhere as a rule — no number, no title — and a duplicated id makes every citation
|
|
421
|
+
// of it ambiguous.
|
|
422
|
+
for (const [kind, pack, file] of canonFiles(SRC)) {
|
|
423
|
+
const rel = path.relative(SRC, file).split(path.sep).join('/');
|
|
391
424
|
let text;
|
|
392
425
|
try { text = readText(file); } catch (e) {
|
|
393
|
-
problems.push(`[authoring]
|
|
394
|
-
+
|
|
395
|
-
+ 'every citation into it, active or not');
|
|
426
|
+
problems.push(`[authoring] ${rel} is ${pack ? 'named in PACK_ORDER' : 'the canon'} but `
|
|
427
|
+
+ `unreadable: ${e.message} — every citation into it breaks`);
|
|
396
428
|
continue;
|
|
397
429
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
problems.push(`[authoring] doctrines/${pack}.md holds a rule addressed to '${named} `
|
|
407
|
-
+ `${n}' — a rule's pack is read from its heading, so this one is unreachable at `
|
|
408
|
-
+ `'${pack}.${n}' and shadows whatever ${named}.md numbers ${n}`);
|
|
430
|
+
let count = 0;
|
|
431
|
+
for (const line of text.split('\n').filter((l) => l.startsWith('### '))) {
|
|
432
|
+
const m = DECL_LINE_RE.exec(line);
|
|
433
|
+
if (!m || m[1] !== kind) {
|
|
434
|
+
problems.push(`[authoring] ${rel}: '${line}' is not a rule declaration — a rule heading `
|
|
435
|
+
+ `here reads '### {{${kind}:<id>}} <Principle Name>'; without an id it has no number, `
|
|
436
|
+
+ 'no title and no way to be cited');
|
|
437
|
+
continue;
|
|
409
438
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
439
|
+
count += 1;
|
|
440
|
+
if (!RULE_ID_RE.test(m[2])) {
|
|
441
|
+
problems.push(`[authoring] ${rel}: rule id '${m[2]}' is not kebab-case (a-z, 0-9, '-')`);
|
|
413
442
|
}
|
|
414
|
-
|
|
415
|
-
|
|
443
|
+
}
|
|
444
|
+
if (!count) {
|
|
445
|
+
const why = pack ? 'the pack would render as an empty section' : 'there would be no invariants';
|
|
446
|
+
problems.push(`[authoring] ${rel} declares no rule — ${why}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const canon = ruleCanon(SRC);
|
|
450
|
+
const seenIds = new Map();
|
|
451
|
+
const seenNames = new Map();
|
|
452
|
+
for (const r of canon.rules) {
|
|
453
|
+
const where = r.kind === 'LAW' ? `law ${r.label}` : `doctrine ${r.label}`;
|
|
454
|
+
if (seenIds.has(r.id)) {
|
|
455
|
+
problems.push(`[authoring] rule id '${r.id}' is declared twice (${seenIds.get(r.id)} and `
|
|
456
|
+
+ `${where}) — ids are unique across laws and doctrines, or a citation names two rules`);
|
|
457
|
+
} else seenIds.set(r.id, where);
|
|
458
|
+
const lower = r.name.toLowerCase();
|
|
459
|
+
if (seenNames.has(lower)) {
|
|
460
|
+
problems.push(`[authoring] principle name '${r.name}' is declared twice (${seenNames.get(lower)} `
|
|
461
|
+
+ `and ${where}) — a citation renders as the name, so two rules would read as one`);
|
|
462
|
+
} else seenNames.set(lower, where);
|
|
463
|
+
const retiredAs = RETIRED_RULE_IDS[r.id];
|
|
464
|
+
if (retiredAs && retiredAs.liveAs !== r.kind) {
|
|
465
|
+
problems.push(`[authoring] ${where} reuses the retired id '${r.id}' (${retiredAs.was}, `
|
|
466
|
+
+ `${retiredAs.note}) — an id that meant one rule in someone's notes must never come back `
|
|
467
|
+
+ 'meaning another; pick a new one');
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const rules = new Map(canon.rules.filter((r) => r.kind === 'DOCTRINE')
|
|
471
|
+
.map((r) => [`${r.pack}.${r.n}`, r]));
|
|
472
|
+
|
|
473
|
+
// ---- the hook-gated rules are still at the address the hooks key them by.
|
|
474
|
+
for (const [id, label] of Object.entries(HOOK_PINNED)) {
|
|
475
|
+
const r = canon.byId.get(id);
|
|
476
|
+
if (!r || r.label !== label) {
|
|
477
|
+
problems.push(`[authoring] the hooks gate '${id}' as ${label} (js/hosts/hooks.mjs ledger keys, `
|
|
478
|
+
+ `settings.mjs CONSENT_RULE, the console's HOOK_GATED), but the canon ${r ? `numbers it ${r.label}`
|
|
479
|
+
: 'no longer declares it'} — move those keys with it, or the tool boundary guards a `
|
|
480
|
+
+ 'different rule than the prompt names');
|
|
481
|
+
}
|
|
416
482
|
}
|
|
417
483
|
|
|
418
484
|
// The console's Principle column, keyed off the rules just parsed — so the gate is driven by
|
|
419
485
|
// the SOURCE and a rule added to a pack file is caught the same day, not when someone
|
|
420
486
|
// notices a blank description in the browser.
|
|
421
|
-
problems.push(...doctrineMetaProblems([...rules.keys()]));
|
|
487
|
+
problems.push(...doctrineMetaProblems([...rules.keys()], [...rules.values()].map((r) => r.id)));
|
|
422
488
|
|
|
423
489
|
// ---- `--exclude-rules` closes against the SAME rules this walk just found.
|
|
424
490
|
//
|
|
@@ -440,41 +506,11 @@ export function constitutionProblems() {
|
|
|
440
506
|
+ 'no switch for it');
|
|
441
507
|
}
|
|
442
508
|
|
|
443
|
-
// ----
|
|
444
|
-
//
|
|
445
|
-
// ⚠ THE ONE HARDCODED ADDRESS IN THE CODEBASE, and the one whose drift is silent AND unsafe.
|
|
446
|
-
// `js/hosts/settings.mjs` keys the git-gate hooks on `process.5` by literal; a renumber of the
|
|
447
|
-
// process pack moves that rule's neighbours under it, and the boundary would then follow
|
|
448
|
-
// whatever rule inherited the number while the prompt still said `process 5`. Checked
|
|
449
|
-
// against the pack file's own title token rather than against prose, so a reworded rule is
|
|
450
|
-
// not a false alarm and a MOVED one is not a silent pass.
|
|
451
|
-
const consentTitle = 'DOC_PROCESS_5';
|
|
452
|
-
let processText = '';
|
|
453
|
-
try { processText = readText(path.join(dir, 'process.md')); } catch { /* reported above */ }
|
|
454
|
-
if (processText && !new RegExp(`^### \\{\\{DOCTRINE\\}\\} process 5 — \\{\\{${consentTitle}\\}\\}`,
|
|
455
|
-
'm').test(processText)) {
|
|
456
|
-
problems.push('[authoring] js/hosts/settings.mjs gates git commit/push on doctrine \'process 5\', '
|
|
457
|
-
+ `but doctrines/process.md does not number {${consentTitle}} 5 — the tool boundary and `
|
|
458
|
-
+ 'the prompt now name different rules');
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// ---- every `{{DOCTRINE}} <pack> <n>` anywhere in src/ resolves to a rule that exists.
|
|
509
|
+
// ---- every citation in src/ is by id, and every id resolves.
|
|
462
510
|
//
|
|
463
|
-
// The whole tree, not the template: a skill footer citing
|
|
464
|
-
// template one, and it
|
|
511
|
+
// The whole tree, not the template: a skill footer citing a rule that does not exist is as
|
|
512
|
+
// broken as a template one, and it renders into every bundle.
|
|
465
513
|
const cited = new Map(); // 'process.5' -> [rel, …]
|
|
466
|
-
// ⚠ A RETIRED LAW KEEPS ITS NUMBER, SO A CITATION OF IT STILL RESOLVES — to "Retired". Laws
|
|
467
|
-
// are appended, never renumbered, which is what keeps ~150 cross-references stable; the price
|
|
468
|
-
// is that nothing else notices `{{LAW}} IX` in a skill that means "ask first" (it is rigor 5
|
|
469
|
-
// now) or `{{LAW}} XI` that means "verify" (folded into III). Read off the canon itself, so a
|
|
470
|
-
// law retired tomorrow is covered the same day.
|
|
471
|
-
const retired = new Set();
|
|
472
|
-
try {
|
|
473
|
-
for (const [, num] of readText(path.join(SRC, 'laws', 'universal.md'))
|
|
474
|
-
.matchAll(/^### \{\{LAW\}\} ([IVXLCDM]+) —[^\n]*\n<!-- LEAN:begin -->\nRetired\b/gm)) {
|
|
475
|
-
retired.add(num);
|
|
476
|
-
}
|
|
477
|
-
} catch { /* an unreadable canon is reported elsewhere */ }
|
|
478
514
|
for (const p of rglob(SRC)) {
|
|
479
515
|
if (!isFile(p) || !(p.endsWith('.md') || p.endsWith('.tmpl'))) continue;
|
|
480
516
|
let text;
|
|
@@ -489,35 +525,33 @@ export function constitutionProblems() {
|
|
|
489
525
|
+ 'reference a toggleable one, or a pack-off build ships a rule pointing at text '
|
|
490
526
|
+ 'that is not there. State the point directly instead');
|
|
491
527
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
528
|
+
for (const [bare] of text.matchAll(BARE_CITE_RE)) {
|
|
529
|
+
problems.push(`[authoring] ${rel} cites '${bare.replace(/\s+/g, ' ')}' by number — numbers are `
|
|
530
|
+
+ 'positional and move when the canon does; cite {{LAW:<id>}} / {{DOCTRINE:<id>}}');
|
|
531
|
+
}
|
|
532
|
+
for (const [whole, kind, id] of text.matchAll(ID_CITE_RE)) {
|
|
533
|
+
const r = canon.byId.get(id);
|
|
534
|
+
if (r && r.kind === kind) {
|
|
535
|
+
if (kind === 'DOCTRINE') {
|
|
536
|
+
const key = `${r.pack}.${r.n}`;
|
|
537
|
+
if (!cited.has(key)) cited.set(key, []);
|
|
538
|
+
cited.get(key).push(rel);
|
|
497
539
|
}
|
|
540
|
+
continue;
|
|
498
541
|
}
|
|
542
|
+
const retiredAs = RETIRED_RULE_IDS[id];
|
|
543
|
+
let why = 'which no law or doctrine declares — it would render as the raw token';
|
|
544
|
+
if (r) why = `but '${id}' is a ${r.kind === 'LAW' ? 'law' : 'doctrine rule'} — use {{${r.kind}:${id}}}`;
|
|
545
|
+
else if (retiredAs) {
|
|
546
|
+
why = `a retired id (${retiredAs.was}, ${retiredAs.note}) — cite the rule its text now lives in`;
|
|
547
|
+
}
|
|
548
|
+
problems.push(`[authoring] ${rel} cites ${whole}, ${why}`);
|
|
499
549
|
}
|
|
500
|
-
for (const [, pack, n] of text.matchAll(DOCTRINE_CITE_RE)) {
|
|
501
|
-
const key = `${pack}.${Number(n)}`;
|
|
502
|
-
if (!cited.has(key)) cited.set(key, []);
|
|
503
|
-
cited.get(key).push(rel);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
for (const [key, where] of [...cited].sort()) {
|
|
507
|
-
if (rules.has(key)) continue;
|
|
508
|
-
problems.push(`[authoring] ${where[0]} cites {{DOCTRINE}} ${key.replace('.', ' ')}, which `
|
|
509
|
-
+ `no pack file defines${where.length > 1 ? ` (and ${where.length - 1} more)` : ''}`);
|
|
510
550
|
}
|
|
511
551
|
|
|
512
|
-
// ---- the vocabulary every theme owes the source.
|
|
513
|
-
const wanted = new Set();
|
|
514
|
-
|
|
515
|
-
let text;
|
|
516
|
-
try { text = readText(path.join(dir, `${pack}.md`)); } catch { continue; }
|
|
517
|
-
for (const [, key] of text.matchAll(DOC_TOKEN_RE)) wanted.add(key);
|
|
518
|
-
}
|
|
519
|
-
const lexWant = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI']
|
|
520
|
-
.map((r) => `LEX_${r}`);
|
|
552
|
+
// ---- the vocabulary every theme owes the source: one title per declared rule, keyed by id.
|
|
553
|
+
const wanted = new Set(canon.rules.filter((r) => r.kind === 'DOCTRINE').map((r) => titleKey(r.kind, r.id)));
|
|
554
|
+
const lexWant = canon.rules.filter((r) => r.kind === 'LAW').map((r) => titleKey(r.kind, r.id));
|
|
521
555
|
// `_TEMPLATE.json` is included — it is the file `--sync-themes` seeds a new theme from, so a
|
|
522
556
|
// stale key there propagates into every voice added afterwards. `themeFiles()` excludes it.
|
|
523
557
|
const files = [...themeFiles(), path.join(THEMES, '_TEMPLATE.json')];
|
|
@@ -532,13 +566,13 @@ export function constitutionProblems() {
|
|
|
532
566
|
const keys = Object.keys(theme);
|
|
533
567
|
for (const key of [...wanted].sort()) {
|
|
534
568
|
if (!Object.hasOwn(theme, key)) {
|
|
535
|
-
problems.push(`[authoring] ${name} has no {${key}} — a doctrine
|
|
569
|
+
problems.push(`[authoring] ${name} has no {${key}} — a doctrine rule declares it, and `
|
|
536
570
|
+ 'theme parity cannot see a key that is missing from every theme at once');
|
|
537
571
|
}
|
|
538
572
|
}
|
|
539
573
|
for (const key of keys.filter((k) => k.startsWith('DOC_')).sort()) {
|
|
540
574
|
if (!wanted.has(key)) {
|
|
541
|
-
problems.push(`[authoring] ${name} defines {${key}}, which no doctrine
|
|
575
|
+
problems.push(`[authoring] ${name} defines {${key}}, which no doctrine rule declares — a `
|
|
542
576
|
+ 'dead voice key ships a title for a rule that does not exist');
|
|
543
577
|
}
|
|
544
578
|
}
|
|
@@ -549,8 +583,8 @@ export function constitutionProblems() {
|
|
|
549
583
|
const stale = lex.filter((k) => !lexWant.includes(k));
|
|
550
584
|
const missing = lexWant.filter((k) => !lex.includes(k));
|
|
551
585
|
for (const k of stale) {
|
|
552
|
-
problems.push(`[authoring] ${name} still defines {${k}} — no law
|
|
553
|
-
+ '
|
|
586
|
+
problems.push(`[authoring] ${name} still defines {${k}} — no law declares this id and `
|
|
587
|
+
+ 'nothing renders this key, so it is a title for a rule that no longer exists');
|
|
554
588
|
}
|
|
555
589
|
for (const k of missing) {
|
|
556
590
|
problems.push(`[authoring] ${name} has no {${k}} — one of the invariants would `
|
|
@@ -670,9 +704,10 @@ export function countTableProblems() {
|
|
|
670
704
|
problems.push(`[authoring] SKILL_CLASS lists '${stale}' but no skills/${stale}.md exists`);
|
|
671
705
|
}
|
|
672
706
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
707
|
+
// The numerals the laws RENDER with — positional, off the canon — because LAW_CLASS and the
|
|
708
|
+
// console's LAW_META are keyed the way the rendered catalogue numbers them.
|
|
709
|
+
const lawRules = ruleCanon(SRC).rules.filter((r) => r.kind === 'LAW');
|
|
710
|
+
const lawNums = lawRules.map((r) => r.label);
|
|
676
711
|
for (const num of lawNums) {
|
|
677
712
|
if (!has(LAW_CLASS, num)) {
|
|
678
713
|
problems.push(`[authoring] laws/universal.md rule ${num} has no class in LAW_CLASS`);
|
|
@@ -686,7 +721,7 @@ export function countTableProblems() {
|
|
|
686
721
|
+ `class ${formatRepr(LAW_CLASSES)}`);
|
|
687
722
|
}
|
|
688
723
|
}
|
|
689
|
-
problems.push(...lawMetaProblems(lawNums, LAW_CLASS, LAW_CLASSES));
|
|
724
|
+
problems.push(...lawMetaProblems(lawNums, LAW_CLASS, LAW_CLASSES, lawRules.map((r) => r.id)));
|
|
690
725
|
|
|
691
726
|
// WHAT SHIPS, NOT WHAT IS FLAT. A folder skill (`src/skills/<name>/SKILL.md`, vendored with
|
|
692
727
|
// its scripts or references) ships exactly like a flat one, so the badge, the README list and
|
|
@@ -868,6 +903,7 @@ export function authoringProblems() {
|
|
|
868
903
|
problems.push(...registryProblems());
|
|
869
904
|
problems.push(...secretProblems());
|
|
870
905
|
problems.push(...vendorPinProblems());
|
|
906
|
+
problems.push(...folderSkillProblems());
|
|
871
907
|
problems.push(...constitutionProblems());
|
|
872
908
|
problems.push(...leanBlockProblems());
|
|
873
909
|
problems.push(...countTableProblems());
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The checks over the REPO's own records — the registry, committed secrets, the hook shim
|
|
3
|
-
* and the vendored
|
|
3
|
+
* and the vendored skill folders (their pins and their SKILL.md frontmatter).
|
|
4
4
|
*
|
|
5
5
|
* Same contract as `checks-build.mjs`: an array of problem strings, empty when clean, one
|
|
6
6
|
* planted fault per check in the gate. This is the group that reads what the repo SAYS about
|
|
@@ -240,6 +240,50 @@ export function vendorPinProblems() {
|
|
|
240
240
|
return problems;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Every vendored skill folder's `SKILL.md` opens with a frontmatter whose `name` IS the
|
|
247
|
+
* folder and which carries a `description`.
|
|
248
|
+
*
|
|
249
|
+
* A flat skill gets that frontmatter from `writeNativeLayer`; a folder is copied through as
|
|
250
|
+
* written, so its own SKILL.md is all a host has to register it from. Two shipped broken:
|
|
251
|
+
* daydream had no frontmatter at all and react-view-transitions named itself after its
|
|
252
|
+
* upstream repo — the Agent Skills format requires the name to match the folder. Claude Code
|
|
253
|
+
* tolerated both, so nothing showed; a stricter host is free not to register them. Only the
|
|
254
|
+
* two keys every host reads are checked, at the top level of the block (an indented
|
|
255
|
+
* `name:` under `metadata:` does not count).
|
|
256
|
+
*/
|
|
257
|
+
export function folderSkillProblems() {
|
|
258
|
+
const problems = [];
|
|
259
|
+
for (const name of VENDORED_SKILL_DIRS) {
|
|
260
|
+
const folder = path.join(SRC, 'skills', name);
|
|
261
|
+
if (!isDir(folder)) continue; // vendorPinProblems reports the missing folder
|
|
262
|
+
let text;
|
|
263
|
+
try { text = readText(path.join(folder, 'SKILL.md')); } catch {
|
|
264
|
+
problems.push(`[authoring] skills/${name}/ has no SKILL.md — no host can load it`);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const fm = FRONTMATTER_RE.exec(text);
|
|
268
|
+
if (!fm) {
|
|
269
|
+
problems.push(`[authoring] skills/${name}/SKILL.md opens with no frontmatter — a host `
|
|
270
|
+
+ 'registers a skill from its name and description');
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const declared = /^name:[ \t]*["']?([^"'\r\n]*?)["']?[ \t]*$/m.exec(fm[1]);
|
|
274
|
+
if (!declared) {
|
|
275
|
+
problems.push(`[authoring] skills/${name}/SKILL.md frontmatter has no 'name'`);
|
|
276
|
+
} else if (declared[1] !== name) {
|
|
277
|
+
problems.push(`[authoring] skills/${name}/SKILL.md is named '${declared[1]}' — the name `
|
|
278
|
+
+ 'must match the folder');
|
|
279
|
+
}
|
|
280
|
+
if (!/^description:[ \t]*\S/m.test(fm[1])) {
|
|
281
|
+
problems.push(`[authoring] skills/${name}/SKILL.md frontmatter has no 'description'`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return problems;
|
|
285
|
+
}
|
|
286
|
+
|
|
243
287
|
|
|
244
288
|
/**
|
|
245
289
|
* `js/README.md` describes every module under `js/`, and only modules that exist.
|