mikoshi-construct 0.3.1 → 0.5.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/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import path22 from "path";
4
+ import path21 from "path";
5
5
  import process6 from "process";
6
6
  import { isTTY } from "@clack/prompts";
7
7
  import { defineCommand, runMain } from "citty";
@@ -46,6 +46,7 @@ function claudeProjectsDir() {
46
46
  }
47
47
  function readUsage(file) {
48
48
  const totals = emptyUsage();
49
+ const counted = /* @__PURE__ */ new Set();
49
50
  for (const line of readFileSync(file, "utf8").split("\n")) {
50
51
  if (!line.startsWith("{"))
51
52
  continue;
@@ -58,6 +59,11 @@ function readUsage(file) {
58
59
  const message = entry.message;
59
60
  if (message?.role !== "assistant" || message.usage == null)
60
61
  continue;
62
+ if (entry.requestId != null) {
63
+ if (counted.has(entry.requestId))
64
+ continue;
65
+ counted.add(entry.requestId);
66
+ }
61
67
  totals.calls += 1;
62
68
  totals.input += message.usage.input_tokens ?? 0;
63
69
  totals.cacheWrite += message.usage.cache_creation_input_tokens ?? 0;
@@ -203,37 +209,37 @@ function toEntry(raw) {
203
209
  }
204
210
  function readLedger(root) {
205
211
  const file = path2.join(root, LEDGER_FILE);
206
- const reading = { entries: [], malformed: [] };
212
+ const reading2 = { entries: [], malformed: [] };
207
213
  if (!existsSync2(file))
208
- return reading;
209
- readFileSync2(file, "utf8").split("\n").forEach((text2, index) => {
214
+ return reading2;
215
+ readFileSync2(file, "utf8").split("\n").forEach((text3, index) => {
210
216
  const line = index + 1;
211
- if (text2.trim() === "")
217
+ if (text3.trim() === "")
212
218
  return;
213
219
  let raw;
214
220
  try {
215
- raw = JSON.parse(text2);
221
+ raw = JSON.parse(text3);
216
222
  } catch {
217
- reading.malformed.push({ line, reason: "not JSON" });
223
+ reading2.malformed.push({ line, reason: "not JSON" });
218
224
  return;
219
225
  }
220
226
  const entry = toEntry(raw);
221
227
  if (typeof entry === "string")
222
- reading.malformed.push({ line, reason: entry });
228
+ reading2.malformed.push({ line, reason: entry });
223
229
  else
224
- reading.entries.push(entry);
230
+ reading2.entries.push(entry);
225
231
  });
226
- return reading;
232
+ return reading2;
227
233
  }
228
- function summarizeLedger(reading) {
229
- const anyTokenUnknown = reading.entries.some((entry) => entry.tokens === "unknown");
230
- const counted = reading.entries.reduce((total, entry) => total + (entry.tokens === "unknown" ? 0 : entry.tokens), 0);
234
+ function summarizeLedger(reading2) {
235
+ const anyTokenUnknown = reading2.entries.some((entry) => entry.tokens === "unknown");
236
+ const counted = reading2.entries.reduce((total, entry) => total + (entry.tokens === "unknown" ? 0 : entry.tokens), 0);
231
237
  return {
232
- runs: reading.entries.length,
233
- agents: reading.entries.reduce((total, entry) => total + entry.agents, 0),
234
- failures: reading.entries.filter((entry) => entry.status !== "done").length,
238
+ runs: reading2.entries.length,
239
+ agents: reading2.entries.reduce((total, entry) => total + entry.agents, 0),
240
+ failures: reading2.entries.filter((entry) => entry.status !== "done").length,
235
241
  tokens: anyTokenUnknown ? "unknown" : counted,
236
- malformed: reading.malformed
242
+ malformed: reading2.malformed
237
243
  };
238
244
  }
239
245
  function withoutTokenTotals(summary) {
@@ -535,8 +541,8 @@ function printCost(ui2, report, last) {
535
541
  // src/commands/cost/index.ts
536
542
  function costReport(cwd, options = {}) {
537
543
  const runtime = resolveRuntime(cwd, options.env ?? process2.env);
538
- const reading = readLedger(cwd);
539
- const ledger = summarizeLedger(reading);
544
+ const reading2 = readLedger(cwd);
545
+ const ledger = summarizeLedger(reading2);
540
546
  const reported = hasLedgerFindings(ledger);
541
547
  const source = runtime === "claude-code" ? new ClaudeCodeCostSource(options.projectsDir) : null;
542
548
  if (source == null || !source.readable())
@@ -547,19 +553,428 @@ function costReport(cwd, options = {}) {
547
553
  runtime,
548
554
  ...result,
549
555
  ...reported ? { ledger } : {},
550
- ...joinable && (reported || result.runs.length > 0) ? { reconciliation: reconcile(reading.entries, result.runs) } : {}
556
+ ...joinable && (reported || result.runs.length > 0) ? { reconciliation: reconcile(reading2.entries, result.runs) } : {}
551
557
  };
552
558
  }
553
559
 
554
- // src/version.ts
555
- import { readFileSync as readFileSync4 } from "fs";
560
+ // src/model/write.ts
561
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
556
562
  import path5 from "path";
563
+
564
+ // src/model/ownership.ts
565
+ var authoredByOwner = (entry) => entry.authoredBy;
566
+
567
+ // src/model/schema.ts
568
+ var MODEL_FILE = "construct.model.json";
569
+ var MODEL_VERSION = 1;
570
+ var FACT_KINDS = ["file-exists", "file-contains"];
571
+ var ENFORCEMENT_LEVELS = ["L0", "L1", "L2", "L3", "L4"];
572
+ var ENTRY_AUTHORS = ["construct", "discovery", "unknown"];
573
+ var FACT_PROPERTIES = ["id", "kind", "path", "authoredBy", "needle"];
574
+ var ENFORCEMENT_PROPERTIES = ["mechanism", "level", "supportedBy"];
575
+ var VERIFICATION_PROPERTIES = ["mechanism", "supportedBy"];
576
+ var CLAIM_PROPERTIES = ["id", "statement", "authoredBy", "enforcement", "verification", "checkId"];
577
+ var HYPOTHESIS_PROPERTIES = ["id", "statement", "authoredBy", "baseSha", "supportedBy"];
578
+ var MODEL_PROPERTIES = ["modelVersion", "facts", "claims", "hypotheses"];
579
+ var DanglingFactReference = class extends Error {
580
+ factId;
581
+ entry;
582
+ constructor(name, entry, factId) {
583
+ super(`${name}: ${entry} supportedBy refers to unknown fact "${factId}"`);
584
+ this.name = "DanglingFactReference";
585
+ this.factId = factId;
586
+ this.entry = entry;
587
+ }
588
+ };
589
+ function fail(name, message) {
590
+ throw new Error(`${name}: ${message}`);
591
+ }
592
+ function isRecord(value) {
593
+ return typeof value === "object" && value != null && !Array.isArray(value);
594
+ }
595
+ function closed(name, record, allowed, where) {
596
+ for (const key of Object.keys(record)) {
597
+ if (!allowed.includes(key))
598
+ fail(name, `${where} carries an unexpected property "${key}": the schema is closed`);
599
+ }
600
+ return record;
601
+ }
602
+ function text(name, record, key, where) {
603
+ const value = record[key];
604
+ if (typeof value !== "string" || value.trim() === "")
605
+ fail(name, `${where} needs a non-empty "${key}"`);
606
+ return value;
607
+ }
608
+ function optionalText(name, record, key, where) {
609
+ if (record[key] === void 0)
610
+ return void 0;
611
+ return text(name, record, key, where);
612
+ }
613
+ function nullableText(name, record, key, where) {
614
+ const value = record[key];
615
+ if (value === null)
616
+ return null;
617
+ if (typeof value !== "string" || value.trim() === "")
618
+ fail(name, `${where} needs a non-empty "${key}" or null`);
619
+ return value;
620
+ }
621
+ function member(name, value, values, key, where) {
622
+ if (!values.includes(value))
623
+ fail(name, `${where} ${key} "${value}" is not one of ${values.join(", ")}`);
624
+ return value;
625
+ }
626
+ function list(name, record, key) {
627
+ const value = record[key];
628
+ if (!Array.isArray(value) || !value.every(isRecord))
629
+ fail(name, `"${key}" must be a list of objects`);
630
+ return value;
631
+ }
632
+ function uniqueIds(name, ids, where) {
633
+ const unique = new Set(ids);
634
+ if (unique.size !== ids.length)
635
+ fail(name, `${where} ids must be unique`);
636
+ return unique;
637
+ }
638
+ function nullableRecord(name, record, key, where) {
639
+ const value = record[key];
640
+ if (value === null)
641
+ return null;
642
+ if (!isRecord(value))
643
+ fail(name, `${where} needs an "${key}" object or null`);
644
+ return value;
645
+ }
646
+ function supportedBy(name, record, where, facts) {
647
+ const value = record.supportedBy;
648
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string" && entry.trim() !== ""))
649
+ fail(name, `${where} needs a "supportedBy" list of fact ids`);
650
+ const ids = value;
651
+ for (const id of ids) {
652
+ if (!facts.has(id))
653
+ throw new DanglingFactReference(name, where, id);
654
+ }
655
+ return ids;
656
+ }
657
+ function parseFacts(name, raw) {
658
+ return list(name, raw, "facts").map((entry, index) => {
659
+ const where = `facts[${index}]`;
660
+ closed(name, entry, FACT_PROPERTIES, where);
661
+ const kind = member(name, text(name, entry, "kind", where), FACT_KINDS, "kind", where);
662
+ const fact = {
663
+ id: text(name, entry, "id", where),
664
+ kind,
665
+ path: text(name, entry, "path", where),
666
+ authoredBy: member(name, text(name, entry, "authoredBy", where), ENTRY_AUTHORS, "authoredBy", where)
667
+ };
668
+ const needle = optionalText(name, entry, "needle", where);
669
+ if (kind === "file-contains") {
670
+ if (needle === void 0)
671
+ fail(name, `${where} of kind "file-contains" needs a non-empty "needle"`);
672
+ fact.needle = needle;
673
+ } else if (needle !== void 0) {
674
+ fail(name, `${where} of kind "file-exists" must not carry a "needle"`);
675
+ }
676
+ return fact;
677
+ });
678
+ }
679
+ function parseClaims(name, raw, facts) {
680
+ return list(name, raw, "claims").map((entry, index) => {
681
+ const where = `claims[${index}]`;
682
+ closed(name, entry, CLAIM_PROPERTIES, where);
683
+ const enforcementEntry = nullableRecord(name, entry, "enforcement", where);
684
+ const verificationEntry = nullableRecord(name, entry, "verification", where);
685
+ const claim = {
686
+ id: text(name, entry, "id", where),
687
+ statement: text(name, entry, "statement", where),
688
+ authoredBy: member(name, text(name, entry, "authoredBy", where), ENTRY_AUTHORS, "authoredBy", where),
689
+ enforcement: enforcementEntry === null ? null : parseEnforcement(name, enforcementEntry, `${where}.enforcement`, facts),
690
+ verification: verificationEntry === null ? null : parseVerification(name, verificationEntry, `${where}.verification`, facts)
691
+ };
692
+ const checkId = optionalText(name, entry, "checkId", where);
693
+ if (checkId !== void 0)
694
+ claim.checkId = checkId;
695
+ return claim;
696
+ });
697
+ }
698
+ function parseEnforcement(name, entry, where, facts) {
699
+ closed(name, entry, ENFORCEMENT_PROPERTIES, where);
700
+ return {
701
+ mechanism: text(name, entry, "mechanism", where),
702
+ level: member(name, text(name, entry, "level", where), ENFORCEMENT_LEVELS, "level", where),
703
+ supportedBy: supportedBy(name, entry, where, facts)
704
+ };
705
+ }
706
+ function parseVerification(name, entry, where, facts) {
707
+ closed(name, entry, VERIFICATION_PROPERTIES, where);
708
+ return {
709
+ mechanism: text(name, entry, "mechanism", where),
710
+ supportedBy: supportedBy(name, entry, where, facts)
711
+ };
712
+ }
713
+ function parseHypotheses(name, raw, facts) {
714
+ return list(name, raw, "hypotheses").map((entry, index) => {
715
+ const where = `hypotheses[${index}]`;
716
+ closed(name, entry, HYPOTHESIS_PROPERTIES, where);
717
+ return {
718
+ id: text(name, entry, "id", where),
719
+ statement: text(name, entry, "statement", where),
720
+ authoredBy: member(name, text(name, entry, "authoredBy", where), ENTRY_AUTHORS, "authoredBy", where),
721
+ baseSha: nullableText(name, entry, "baseSha", where),
722
+ supportedBy: supportedBy(name, entry, where, facts)
723
+ };
724
+ });
725
+ }
726
+ function parseModel(source, name) {
727
+ let raw;
728
+ try {
729
+ raw = JSON.parse(source);
730
+ } catch (error) {
731
+ fail(name, `the document must be JSON: ${error.message}`);
732
+ }
733
+ if (!isRecord(raw))
734
+ fail(name, "the document must be an object");
735
+ closed(name, raw, MODEL_PROPERTIES, "the document");
736
+ if (raw.modelVersion !== MODEL_VERSION)
737
+ fail(name, `the document needs "modelVersion": ${MODEL_VERSION}`);
738
+ const facts = parseFacts(name, raw);
739
+ const factIds = uniqueIds(name, facts.map((fact) => fact.id), "fact");
740
+ const claims = parseClaims(name, raw, factIds);
741
+ uniqueIds(name, claims.map((claim) => claim.id), "claim");
742
+ const hypotheses = parseHypotheses(name, raw, factIds);
743
+ uniqueIds(name, hypotheses.map((hypothesis) => hypothesis.id), "hypothesis");
744
+ return { modelVersion: MODEL_VERSION, facts, claims, hypotheses };
745
+ }
746
+
747
+ // src/model/write.ts
748
+ var SECURITY_WORKFLOW = ".github/workflows/security.yml";
749
+ var CI_WORKFLOW = ".github/workflows/ci.yml";
750
+ var CONTRACT_WORKFLOW = ".github/workflows/api-contract.yml";
751
+ var MANIFEST = "package.json";
752
+ var LINT_POLICY_TEST = "scripts/tests/lint/syntax-policy.test.ts";
753
+ function baselineFacts(harnessCommand) {
754
+ return [
755
+ { id: "security-workflow", kind: "file-exists", path: SECURITY_WORKFLOW, authoredBy: "construct" },
756
+ { id: "security-workflow-runs-gitleaks", kind: "file-contains", path: SECURITY_WORKFLOW, authoredBy: "construct", needle: "gitleaks" },
757
+ { id: "gitleaks-config", kind: "file-exists", path: ".gitleaks.toml", authoredBy: "construct" },
758
+ { id: "security-workflow-audits-dependencies", kind: "file-contains", path: SECURITY_WORKFLOW, authoredBy: "construct", needle: "pnpm audit --audit-level=high" },
759
+ { id: "ci-workflow", kind: "file-exists", path: CI_WORKFLOW, authoredBy: "construct" },
760
+ { id: "ci-workflow-runs-the-harness", kind: "file-contains", path: CI_WORKFLOW, authoredBy: "construct", needle: harnessCommand },
761
+ { id: "eslint-config", kind: "file-exists", path: "eslint.config.mjs", authoredBy: "construct" },
762
+ { id: "security-invariants", kind: "file-exists", path: "architecture/security-invariants.md", authoredBy: "construct" },
763
+ { id: "harness-manifest", kind: "file-exists", path: MANIFEST, authoredBy: "construct" },
764
+ { id: "harness-script-runs-lint", kind: "file-contains", path: MANIFEST, authoredBy: "construct", needle: "pnpm lint" },
765
+ { id: "harness-script-runs-typecheck", kind: "file-contains", path: MANIFEST, authoredBy: "construct", needle: "pnpm typecheck" },
766
+ { id: "harness-script-runs-tests", kind: "file-contains", path: MANIFEST, authoredBy: "construct", needle: "pnpm test" }
767
+ ];
768
+ }
769
+ function listed(items) {
770
+ return items.length < 2 ? items.join("") : `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
771
+ }
772
+ function harnessStepsClaim(harnessCommand, contracts) {
773
+ const steps = [...contracts ? ["contracts:check"] : [], "lint", "typecheck", "tests"];
774
+ const spelled = [...contracts ? ["pnpm contracts:check"] : [], "pnpm lint", "pnpm typecheck", "pnpm test"];
775
+ return {
776
+ id: "harness-steps",
777
+ statement: `${harnessCommand} runs ${listed(steps)}, rather than merely existing as a script`,
778
+ authoredBy: "construct",
779
+ enforcement: {
780
+ mechanism: `${CI_WORKFLOW} runs ${harnessCommand} on every pull request, and ${MANIFEST} spells that command out as ${listed(spelled)}`,
781
+ level: "L3",
782
+ supportedBy: [
783
+ "ci-workflow-runs-the-harness",
784
+ ...contracts ? ["harness-script-runs-contracts-check"] : [],
785
+ "harness-script-runs-lint",
786
+ "harness-script-runs-typecheck",
787
+ "harness-script-runs-tests"
788
+ ]
789
+ },
790
+ verification: {
791
+ mechanism: `${MANIFEST} is the file that command resolves against, so a step dropped from it is visible there`,
792
+ supportedBy: ["harness-manifest"]
793
+ }
794
+ };
795
+ }
796
+ function baselineClaims(harnessCommand, contracts) {
797
+ return [
798
+ {
799
+ id: "no-committed-secret",
800
+ statement: "No secret, token or connection string is committed to this repository, including into gitignored files",
801
+ authoredBy: "construct",
802
+ enforcement: {
803
+ mechanism: `${SECURITY_WORKFLOW} runs gitleaks over the history on every push and pull request`,
804
+ level: "L3",
805
+ supportedBy: ["security-workflow", "security-workflow-runs-gitleaks"]
806
+ },
807
+ verification: {
808
+ mechanism: ".gitleaks.toml keeps the default ruleset live for that scan",
809
+ supportedBy: ["gitleaks-config"]
810
+ }
811
+ },
812
+ {
813
+ id: "vulnerable-dependencies-are-visible",
814
+ statement: "Dependencies with known high-severity vulnerabilities are visible",
815
+ authoredBy: "construct",
816
+ enforcement: {
817
+ mechanism: "security.yml runs pnpm audit weekly and on pull requests, reporting only",
818
+ level: "L3",
819
+ supportedBy: ["security-workflow", "security-workflow-audits-dependencies"]
820
+ },
821
+ verification: {
822
+ mechanism: "architecture/security-invariants.md names the mechanism behind each invariant",
823
+ supportedBy: ["security-invariants"]
824
+ }
825
+ },
826
+ {
827
+ id: "every-change-passes-the-harness",
828
+ statement: "Lint, typecheck and tests pass on every change, as one command",
829
+ authoredBy: "construct",
830
+ enforcement: {
831
+ mechanism: `${CI_WORKFLOW} runs ${harnessCommand} on every pull request and push to main`,
832
+ level: "L3",
833
+ supportedBy: ["ci-workflow", "ci-workflow-runs-the-harness"]
834
+ },
835
+ verification: {
836
+ mechanism: "eslint.config.mjs is the single source of style for that run",
837
+ supportedBy: ["eslint-config"]
838
+ },
839
+ checkId: "ci"
840
+ },
841
+ harnessStepsClaim(harnessCommand, contracts)
842
+ ];
843
+ }
844
+ function sampleFacts() {
845
+ return [
846
+ { id: "lint-policy-test", kind: "file-exists", path: LINT_POLICY_TEST, authoredBy: "construct" },
847
+ { id: "lint-policy-test-loads-eslint", kind: "file-contains", path: LINT_POLICY_TEST, authoredBy: "construct", needle: "import { ESLint } from 'eslint'" }
848
+ ];
849
+ }
850
+ function sampleClaims(harnessCommand) {
851
+ return [
852
+ {
853
+ id: "lint-policy",
854
+ statement: "The lint policy this repository declares is itself checked by a test, not only applied by the linter",
855
+ authoredBy: "construct",
856
+ enforcement: {
857
+ mechanism: `${LINT_POLICY_TEST} asserts the restrictions the lint policy declares, and ${CI_WORKFLOW} runs ${harnessCommand} over it on every pull request`,
858
+ level: "L3",
859
+ supportedBy: ["lint-policy-test", "ci-workflow-runs-the-harness"]
860
+ },
861
+ verification: {
862
+ mechanism: `${LINT_POLICY_TEST} resolves eslint.config.mjs through the ESLint API rather than reading its text`,
863
+ supportedBy: ["lint-policy-test-loads-eslint", "eslint-config"]
864
+ },
865
+ checkId: "lint-policy"
866
+ }
867
+ ];
868
+ }
869
+ function contractFacts(contractPath) {
870
+ return [
871
+ { id: "contract-workflow", kind: "file-exists", path: CONTRACT_WORKFLOW, authoredBy: "construct" },
872
+ { id: "contract-workflow-fails-on-a-breaking-change", kind: "file-contains", path: CONTRACT_WORKFLOW, authoredBy: "construct", needle: "fail-on: ERR" },
873
+ { id: "api-contract", kind: "file-exists", path: contractPath, authoredBy: "construct" },
874
+ { id: "harness-script-runs-contracts-check", kind: "file-contains", path: MANIFEST, authoredBy: "construct", needle: "pnpm contracts:check" }
875
+ ];
876
+ }
877
+ function contractClaims(contractPath) {
878
+ return [
879
+ {
880
+ id: "a-breaking-api-change-is-named-before-it-ships",
881
+ statement: "A change that breaks the HTTP API is identified on the pull request that makes it, never discovered afterwards",
882
+ authoredBy: "construct",
883
+ enforcement: {
884
+ mechanism: `${CONTRACT_WORKFLOW} runs oasdiff against the base branch and fails on a breaking change`,
885
+ level: "L3",
886
+ supportedBy: ["contract-workflow", "contract-workflow-fails-on-a-breaking-change"]
887
+ },
888
+ verification: {
889
+ mechanism: `${contractPath} is the contract that comparison reads`,
890
+ supportedBy: ["api-contract"]
891
+ }
892
+ }
893
+ ];
894
+ }
895
+ function buildModel(input) {
896
+ const { harnessCommand, contractPath } = input.vars;
897
+ return {
898
+ modelVersion: MODEL_VERSION,
899
+ facts: [...baselineFacts(harnessCommand), ...input.contracts ? contractFacts(contractPath) : [], ...input.sample ? sampleFacts() : []],
900
+ claims: [...baselineClaims(harnessCommand, input.contracts), ...input.contracts ? contractClaims(contractPath) : [], ...input.sample ? sampleClaims(harnessCommand) : []],
901
+ hypotheses: []
902
+ };
903
+ }
904
+ function readModel(root) {
905
+ const file = path5.join(root, MODEL_FILE);
906
+ if (!existsSync5(file))
907
+ return null;
908
+ try {
909
+ return parseModel(readFileSync4(file, "utf8"), MODEL_FILE);
910
+ } catch (error) {
911
+ if (error instanceof DanglingFactReference)
912
+ throw new Error(`${MODEL_FILE} stands on a fact that is not in it: ${error.entry} names "${error.factId}", which no fact declares. Nothing was written and ${MODEL_FILE} was not replaced: put the fact "${error.factId}" back, or drop it from ${error.entry}, and run this again.`);
913
+ throw error;
914
+ }
915
+ }
916
+ function mergeEntries(existing, fresh, keepDropped) {
917
+ const rebuilt = new Map(fresh.map((entry) => [entry.id, entry]));
918
+ const survivors = existing.flatMap((entry) => {
919
+ if (authoredByOwner(entry) !== "construct")
920
+ return [entry];
921
+ const replacement = rebuilt.get(entry.id);
922
+ if (replacement !== void 0)
923
+ return [replacement];
924
+ return keepDropped(entry) ? [entry] : [];
925
+ });
926
+ const present = new Set(survivors.map((entry) => entry.id));
927
+ return [...survivors, ...fresh.filter((entry) => !present.has(entry.id))];
928
+ }
929
+ function factsStoodOn(claims, hypotheses) {
930
+ const stoodOn = /* @__PURE__ */ new Map();
931
+ const record = (factId, entryId) => {
932
+ stoodOn.set(factId, [...stoodOn.get(factId) ?? [], entryId]);
933
+ };
934
+ for (const claim of claims) {
935
+ for (const factId of [...claim.enforcement?.supportedBy ?? [], ...claim.verification?.supportedBy ?? []])
936
+ record(factId, claim.id);
937
+ }
938
+ for (const hypothesis of hypotheses) {
939
+ for (const factId of hypothesis.supportedBy)
940
+ record(factId, hypothesis.id);
941
+ }
942
+ return stoodOn;
943
+ }
944
+ function mergeModel(existing, fresh) {
945
+ if (existing == null)
946
+ return { model: fresh, retained: [] };
947
+ const claims = mergeEntries(existing.claims, fresh.claims, () => false);
948
+ const hypotheses = mergeEntries(existing.hypotheses, fresh.hypotheses, () => false);
949
+ const stoodOn = factsStoodOn(claims, hypotheses);
950
+ const rebuilt = new Set(fresh.facts.map((fact) => fact.id));
951
+ const retained = existing.facts.filter((fact) => authoredByOwner(fact) === "construct" && !rebuilt.has(fact.id) && stoodOn.has(fact.id)).map((fact) => ({ id: fact.id, stoodOnBy: stoodOn.get(fact.id) ?? [] }));
952
+ return {
953
+ model: {
954
+ modelVersion: MODEL_VERSION,
955
+ facts: mergeEntries(existing.facts, fresh.facts, (fact) => stoodOn.has(fact.id)),
956
+ claims,
957
+ hypotheses
958
+ },
959
+ retained
960
+ };
961
+ }
962
+ function writeModel(root, model) {
963
+ const source = `${JSON.stringify(model, null, 2)}
964
+ `;
965
+ parseModel(source, MODEL_FILE);
966
+ writeFileSync2(path5.join(root, MODEL_FILE), source);
967
+ }
968
+
969
+ // src/version.ts
970
+ import { readFileSync as readFileSync5 } from "fs";
971
+ import path6 from "path";
557
972
  import { fileURLToPath } from "url";
558
- var HERE = path5.dirname(fileURLToPath(import.meta.url));
973
+ var HERE = path6.dirname(fileURLToPath(import.meta.url));
559
974
  function readVersion() {
560
975
  for (const candidate of ["../package.json", "../../package.json"]) {
561
976
  try {
562
- const parsed = JSON.parse(readFileSync4(path5.resolve(HERE, candidate), "utf8"));
977
+ const parsed = JSON.parse(readFileSync5(path6.resolve(HERE, candidate), "utf8"));
563
978
  if (parsed.name === "mikoshi-construct" && parsed.version != null)
564
979
  return parsed.version;
565
980
  } catch {
@@ -570,219 +985,80 @@ function readVersion() {
570
985
  var VERSION = readVersion();
571
986
 
572
987
  // src/commands/doctor/baseline.ts
573
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
574
- import path6 from "path";
575
- function baselineVerdict(root, manifest) {
576
- const missingFiles = [];
577
- const modifiedFiles = [];
578
- for (const [file, hash] of Object.entries(manifest.files)) {
579
- const absolute = path6.join(root, file);
580
- if (!existsSync5(absolute))
581
- missingFiles.push(file);
582
- else if (sha256(readFileSync5(absolute, "utf8")) !== hash)
583
- modifiedFiles.push(file);
584
- }
585
- return { missingFiles, modifiedFiles };
586
- }
587
-
588
- // src/commands/doctor/enforcement.ts
589
- function harnessReach(evidence) {
590
- const command = `"${evidence.harness.command}"`;
591
- if (evidence.workflows.harnessWorkflow != null)
592
- return { level: "L3", evidence: `${evidence.workflows.harnessWorkflow} runs ${command}` };
593
- if (evidence.hooks.runsHarness && evidence.hooks.manager != null)
594
- return { level: "L2", evidence: `${evidence.hooks.manager} runs ${command}, and a local hook is bypassable with --no-verify` };
595
- return { level: "L0", evidence: `no workflow and no hook configuration runs ${command}` };
596
- }
597
- function reached(id, state, evidence, detail) {
598
- const reach = harnessReach(evidence);
599
- return { id, level: reach.level, state, evidence: `${detail}; ${reach.evidence}` };
600
- }
601
- function absent(id, detail) {
602
- return { id, level: "L0", state: "absent", evidence: detail };
603
- }
604
-
605
- // src/commands/doctor/checks/ci.ts
606
- var ID = "ci";
607
- var SCOPE = "branch protection and organisation rulesets live in the GitHub API, not in the repository, so doctor cannot see whether this blocks a merge";
608
- function ciCheck(evidence) {
609
- const workflows = evidence.workflows;
610
- const reach = harnessReach(evidence);
611
- if (workflows.harnessWorkflow != null)
612
- return { id: ID, level: reach.level, state: "present", evidence: `${reach.evidence}; ${SCOPE}` };
613
- const seen = workflows.files.length === 0 ? `${workflows.directory} holds no workflow file` : `none of ${workflows.files.map((file) => `${workflows.directory}/${file}`).join(", ")} runs "${evidence.harness.command}"`;
614
- return { id: ID, level: reach.level, state: "unknown", evidence: `${seen}; ${SCOPE}` };
615
- }
988
+ import { existsSync as existsSync7 } from "fs";
989
+ import path8 from "path";
616
990
 
617
- // src/commands/doctor/runner.ts
618
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
991
+ // src/commands/doctor/readings.ts
992
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
619
993
  import path7 from "path";
620
- var RUNNER_CONFIG_FILES = [
621
- "vitest.config.ts",
622
- "vitest.config.mts",
623
- "vitest.config.js",
624
- "vitest.config.mjs",
625
- "vite.config.ts",
626
- "vite.config.mts",
627
- "vite.config.js",
628
- "vite.config.mjs"
629
- ];
630
- var STRING_LITERAL = /^(['"])(.*)\1$/;
631
- function literalEntries(body) {
632
- const trimmed = body.trim();
633
- if (trimmed === "")
634
- return [];
635
- const entries = trimmed.split(",").map((entry) => entry.trim()).filter((entry) => entry !== "");
636
- const values = entries.map((entry) => STRING_LITERAL.exec(entry)?.[2]);
637
- return values.every((value) => value != null) ? values : null;
638
- }
639
- function includeGlobs(source) {
640
- const found = [];
641
- let literal = false;
642
- for (const match of source.matchAll(/include\s*:\s*/g)) {
643
- const rest = source.slice(match.index + match[0].length);
644
- if (!rest.startsWith("["))
645
- return null;
646
- const close = rest.indexOf("]");
647
- if (close === -1)
994
+ function cause(error) {
995
+ return error instanceof Error ? error.message : String(error);
996
+ }
997
+ var FileReadings = class {
998
+ constructor(root) {
999
+ this.root = root;
1000
+ }
1001
+ root;
1002
+ causes = /* @__PURE__ */ new Map();
1003
+ read(file) {
1004
+ const target = path7.join(this.root, file);
1005
+ if (!existsSync6(target))
648
1006
  return null;
649
- const entries = literalEntries(rest.slice(1, close));
650
- if (entries == null)
1007
+ try {
1008
+ return readFileSync6(target, "utf8");
1009
+ } catch (error) {
1010
+ this.causes.set(file, cause(error));
651
1011
  return null;
652
- literal = true;
653
- found.push(...entries);
654
- }
655
- return literal ? found : null;
656
- }
657
- function escapeLiteral(character) {
658
- return /[.+^${}()|[\]\\]/.test(character) ? `\\${character}` : character;
659
- }
660
- function globToRegExp(glob) {
661
- let pattern = "";
662
- let braces = 0;
663
- for (let index = 0; index < glob.length; index += 1) {
664
- const character = glob[index];
665
- if (character === "*" && glob[index + 1] === "*" && glob[index + 2] === "/") {
666
- pattern += "(?:[^/]+/)*";
667
- index += 2;
668
- continue;
669
- }
670
- if (character === "*" && glob[index + 1] === "*") {
671
- pattern += ".*";
672
- index += 1;
673
- continue;
674
- }
675
- if (character === "*") {
676
- pattern += "[^/]*";
677
- continue;
678
- }
679
- if (character === "?") {
680
- pattern += "[^/]";
681
- continue;
682
- }
683
- if (character === "{") {
684
- braces += 1;
685
- pattern += "(?:";
686
- continue;
687
1012
  }
688
- if (character === "}" && braces > 0) {
689
- braces -= 1;
690
- pattern += ")";
691
- continue;
1013
+ }
1014
+ readJson(file) {
1015
+ const source = this.read(file);
1016
+ if (source == null)
1017
+ return null;
1018
+ try {
1019
+ return JSON.parse(source);
1020
+ } catch (error) {
1021
+ this.causes.set(file, cause(error));
1022
+ return null;
692
1023
  }
693
- if (character === "," && braces > 0) {
694
- pattern += "|";
695
- continue;
1024
+ }
1025
+ entries(directory) {
1026
+ const target = path7.join(this.root, directory);
1027
+ if (!existsSync6(target))
1028
+ return null;
1029
+ try {
1030
+ return readdirSync3(target);
1031
+ } catch (error) {
1032
+ this.causes.set(directory, cause(error));
1033
+ return null;
696
1034
  }
697
- pattern += escapeLiteral(character);
698
1035
  }
699
- return new RegExp(`^${pattern}$`);
700
- }
701
- function matchesAnyGlob(file, globs) {
702
- const normalized = file.replace(/^\.\//, "");
703
- return globs.some((glob) => globToRegExp(glob.replace(/^\.\//, "")).test(normalized));
704
- }
705
- function readRunnerFacts(root, harnessText) {
706
- const invokedByHarness = harnessText.includes("vitest");
707
- const file = RUNNER_CONFIG_FILES.find((candidate) => existsSync6(path7.join(root, candidate))) ?? null;
708
- if (file == null) {
709
- return {
710
- file: null,
711
- globs: null,
712
- note: `no runner config file (${RUNNER_CONFIG_FILES[0]} or a sibling) exists, so the include list cannot be read`,
713
- invokedByHarness
714
- };
1036
+ unreadable(file) {
1037
+ return this.causes.has(file);
715
1038
  }
716
- let source;
717
- try {
718
- source = readFileSync6(path7.join(root, file), "utf8");
719
- } catch {
720
- return { file, globs: null, note: `${file} cannot be read, so the include list is unknown`, invokedByHarness };
1039
+ get files() {
1040
+ return [...this.causes.keys()].sort().map((file) => `${file} (${this.causes.get(file) ?? ""})`);
721
1041
  }
722
- const globs = includeGlobs(source);
723
- if (globs == null)
724
- return { file, globs: null, note: `the include in ${file} is not a literal list of strings, so doctor cannot say what the runner collects`, invokedByHarness };
725
- return { file, globs, note: `${file} includes ${globs.map((glob) => `"${glob}"`).join(", ")}`, invokedByHarness };
726
- }
727
-
728
- // src/commands/doctor/checks/construct-tests.ts
729
- var ID2 = "construct-tests";
730
- function constructTestsCheck(evidence) {
731
- const recorded = evidence.recordedTests;
732
- if (recorded.length === 0)
733
- return absent(ID2, "construct.json records no test file; weakest link: there are no construct tests to run");
734
- const runner = evidence.runner;
735
- if (runner.globs == null)
736
- return reached(ID2, "unknown", evidence, `construct.json records ${recorded.length} test files, but ${runner.note}`);
737
- const orphan = recorded.find((file) => !matchesAnyGlob(file, runner.globs ?? []));
738
- if (orphan != null)
739
- return absent(ID2, `${orphan} is recorded in construct.json, but ${runner.note}, so the runner never collects it; weakest link: a construct test that nothing runs`);
740
- if (!runner.invokedByHarness)
741
- return absent(ID2, `construct.json records ${recorded.length} test files and ${runner.note}, but "${evidence.harness.script}" does not run the test runner; weakest link: the harness command never reaches them`);
742
- return reached(ID2, "present", evidence, `all ${recorded.length} test files recorded in construct.json match the include in ${runner.file}, and "${evidence.harness.script}" runs the test runner`);
743
- }
744
-
745
- // src/commands/doctor/checks/hook.ts
746
- var ID3 = "hook";
747
- function hookCheck(evidence) {
748
- const hooks = evidence.hooks;
749
- if (hooks.manager != null) {
750
- const runs = hooks.runsHarness ? `runs "${evidence.harness.command}"` : `does not run "${evidence.harness.command}"`;
751
- return { id: ID3, level: "L2", state: "present", evidence: `${hooks.manager} installs a git hook and ${runs}; a local hook is bypassable with --no-verify` };
752
- }
753
- if (hooks.script != null)
754
- return { id: ID3, level: "L0", state: "present", evidence: `package.json script "${hooks.script}" is claimed as a guard, but no .husky, lefthook, simple-git-hooks or core.hooksPath configuration installs it; weakest link: nobody is obliged to run it` };
755
- return { id: ID3, level: "L0", state: "absent", evidence: "no .husky, lefthook, simple-git-hooks or core.hooksPath configuration and no pre-commit script in package.json" };
756
- }
757
-
758
- // src/commands/doctor/checks/lint-policy.ts
759
- var ID4 = "lint-policy";
760
- function lintPolicyCheck(evidence) {
761
- const policy = evidence.policyTests[0];
762
- if (policy == null) {
763
- if (evidence.unreadable.length > 0)
764
- return reached(ID4, "unknown", evidence, `${evidence.unreadable[0]} cannot be read, so doctor cannot say whether a lint policy check exists`);
765
- return absent(ID4, "no file recorded in construct.json runs ESLint over the lint policy this repository declares; weakest link: the repository has no policy check to run");
766
- }
767
- const runner = evidence.runner;
768
- if (runner.globs == null)
769
- return reached(ID4, "unknown", evidence, `${policy} runs ESLint over the lint policy this repository declares, but ${runner.note}`);
770
- if (!matchesAnyGlob(policy, runner.globs))
771
- return absent(ID4, `${policy} runs ESLint over the lint policy this repository declares, but ${runner.note}; weakest link: the runner never collects the policy check`);
772
- if (!runner.invokedByHarness)
773
- return absent(ID4, `${policy} runs ESLint over the lint policy this repository declares and ${runner.note}, but "${evidence.harness.script}" does not run the test runner; weakest link: the harness command never reaches the policy check`);
774
- return reached(ID4, "present", evidence, `${policy} runs ESLint over the lint policy this repository declares, ${runner.note}, and "${evidence.harness.script}" runs the test runner`);
775
- }
776
-
777
- // src/commands/doctor/checks/red-gate.ts
778
- var ID5 = "red-gate";
779
- function redGateCheck(evidence) {
780
- return reached(ID5, "unknown", evidence, `doctor executes nothing from the repository it inspects, so whether "${evidence.harness.command}" passes on a clean checkout is unproven here; CI is where that is proven`);
1042
+ };
1043
+
1044
+ // src/commands/doctor/baseline.ts
1045
+ function baselineVerdict(root, manifest, readings = new FileReadings(root)) {
1046
+ const missingFiles = [];
1047
+ const modifiedFiles = [];
1048
+ for (const [file, hash] of Object.entries(manifest.files)) {
1049
+ if (!existsSync7(path8.join(root, file))) {
1050
+ missingFiles.push(file);
1051
+ continue;
1052
+ }
1053
+ const content = readings.read(file);
1054
+ if (content != null && sha256(content) !== hash)
1055
+ modifiedFiles.push(file);
1056
+ }
1057
+ return { missingFiles, modifiedFiles };
781
1058
  }
782
1059
 
783
1060
  // src/commands/doctor/discovery.ts
784
- import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
785
- import path8 from "path";
1061
+ import path9 from "path";
786
1062
 
787
1063
  // src/detect/facts.ts
788
1064
  function factsTheRepositoryEstablishes(root) {
@@ -806,22 +1082,20 @@ function blockBody(document, marker) {
806
1082
  const body = document.slice(start + markerOpen(marker).length, stop).trim();
807
1083
  return body === "" || body === DISCOVERY_PLACEHOLDER ? null : body;
808
1084
  }
809
- function compositionBody(directory) {
810
- if (!existsSync7(directory))
811
- return null;
812
- const models = readdirSync3(directory).filter((file) => file.endsWith(".yaml")).sort();
813
- if (models.length === 0)
814
- return null;
815
- return models.map((model) => `${model}
816
- ${readFileSync7(path8.join(directory, model), "utf8")}`).join("\n");
1085
+ function compositionBody(directory, readings) {
1086
+ const models = (readings.entries(directory) ?? []).filter((file) => file.endsWith(".yaml")).sort();
1087
+ const bodies = models.flatMap((model) => {
1088
+ const source = readings.read(path9.posix.join(directory, model));
1089
+ return source == null ? [] : [`${model}
1090
+ ${source}`];
1091
+ });
1092
+ return bodies.length === 0 ? null : bodies.join("\n");
817
1093
  }
818
- function markerBody(root, marker, file) {
819
- const location = path8.join(root, file);
1094
+ function markerBody(root, marker, file, readings = new FileReadings(root)) {
820
1095
  if (marker === "composition")
821
- return compositionBody(location);
822
- if (!existsSync7(location))
823
- return null;
824
- return blockBody(readFileSync7(location, "utf8"), marker);
1096
+ return compositionBody(file, readings);
1097
+ const source = readings.read(file);
1098
+ return source == null ? null : blockBody(source, marker);
825
1099
  }
826
1100
  function markerFileFor(root, manifest, marker) {
827
1101
  const recordedFile = manifest.discovery.markers[marker].file;
@@ -832,18 +1106,18 @@ function markerFileFor(root, manifest, marker) {
832
1106
  return decided;
833
1107
  return factsTheRepositoryEstablishes(root).compositionDir ?? recordedFile;
834
1108
  }
835
- function missingDiscovery(root, manifest) {
836
- return DISCOVERY_MARKERS.filter((marker) => markerBody(root, marker, markerFileFor(root, manifest, marker)) == null);
1109
+ function missingDiscovery(root, manifest, readings = new FileReadings(root)) {
1110
+ return DISCOVERY_MARKERS.filter((marker) => markerBody(root, marker, markerFileFor(root, manifest, marker), readings) == null);
837
1111
  }
838
1112
 
839
- // src/commands/doctor/evidence.ts
840
- import { readFileSync as readFileSync11 } from "fs";
841
- import path12 from "path";
1113
+ // src/commands/doctor/families.ts
1114
+ function isIntact(evidence) {
1115
+ return evidence.missingFiles.length === 0 && evidence.harnessProblems.length === 0 && evidence.unreadableFiles.length === 0;
1116
+ }
842
1117
 
843
1118
  // src/commands/doctor/harness.ts
844
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
845
- import path9 from "path";
846
- var REQUIRED_QUALITY_STEPS = ["lint", "typecheck", "test"];
1119
+ import { existsSync as existsSync8 } from "fs";
1120
+ import path10 from "path";
847
1121
  var SCRIPT_REFERENCE = /(?:^|&&|\|\||;)\s*(?:pnpm|npm|yarn|bun)\s+(?:run\s+)?([\w:.-]+)/g;
848
1122
  function harnessScriptName(command) {
849
1123
  return command.replace(/^(pnpm|npm|yarn|bun)\s+(run\s+)?/, "");
@@ -858,163 +1132,142 @@ function expandScript(scripts, name, seen) {
858
1132
  const referenced = [...body.matchAll(SCRIPT_REFERENCE)].map((match) => match[1]);
859
1133
  return [body, ...referenced.map((reference) => expandScript(scripts, reference, seen))].join(" && ");
860
1134
  }
861
- function readHarnessFacts(root, command) {
1135
+ var HARNESS_MANIFEST = "package.json";
1136
+ function readHarnessFacts(root, command, readings = new FileReadings(root)) {
862
1137
  const script = harnessScriptName(command);
863
- const manifestPath = path9.join(root, "package.json");
864
- const packageJson = existsSync8(manifestPath) ? JSON.parse(readFileSync8(manifestPath, "utf8")) : null;
1138
+ const packageJson = readings.readJson(HARNESS_MANIFEST);
865
1139
  const scripts = packageJson?.scripts ?? {};
866
1140
  const body = scripts[script] ?? null;
867
1141
  return {
868
1142
  command,
869
1143
  script,
870
- scripts,
871
1144
  body,
872
1145
  resolved: expandScript(scripts, script, /* @__PURE__ */ new Set()),
873
- commandForms: [command, `pnpm run ${script}`, `pnpm ${script}`, `npm run ${script}`, `yarn ${script}`],
874
1146
  packageJson
875
1147
  };
876
1148
  }
877
- function runsHarnessCommand(text2, forms) {
878
- return forms.some((form) => text2.includes(form));
879
- }
880
- function contractProblems(root, contracts, script, body) {
1149
+ function missingContractFiles(root, contracts) {
881
1150
  if (contracts == null)
882
1151
  return [];
883
- const problems = [contracts.path, contracts.types].filter((file) => !existsSync8(path9.join(root, file))).map((file) => `${file} is missing (construct.json \u2192 contracts)`);
884
- if (!body.includes("contracts:check"))
885
- problems.push(`"${script}" does not run contracts:check`);
886
- return problems;
1152
+ return [contracts.path, contracts.types].filter((file) => !existsSync8(path10.join(root, file))).map((file) => `${file} is missing (construct.json \u2192 contracts)`);
887
1153
  }
888
- function harnessProblems(root, manifest, facts) {
1154
+ function harnessProblems(root, manifest, facts, readings = new FileReadings(root)) {
889
1155
  if (facts.packageJson == null)
890
- return ["package.json is missing"];
891
- const body = facts.body;
892
- if (body == null)
1156
+ return readings.unreadable(HARNESS_MANIFEST) ? [] : [`${HARNESS_MANIFEST} is missing`];
1157
+ if (facts.body == null)
893
1158
  return [`package.json has no "${facts.script}" script (harness command is "${facts.command}")`];
894
- return [
895
- ...REQUIRED_QUALITY_STEPS.filter((step) => !body.includes(step)).map((step) => `"${facts.script}" does not run ${step}`),
896
- ...contractProblems(root, manifest.contracts, facts.script, body)
897
- ];
1159
+ return missingContractFiles(root, manifest.contracts);
898
1160
  }
899
1161
 
900
- // src/commands/doctor/hooks.ts
901
- import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync2 } from "fs";
902
- import path10 from "path";
903
- var LEFTHOOK_FILES = ["lefthook.yml", "lefthook.yaml", "lefthook.toml", "lefthook.json", ".lefthook.yml", ".lefthook.yaml"];
904
- var SIMPLE_GIT_HOOKS_FILES = [".simple-git-hooks.js", ".simple-git-hooks.cjs", ".simple-git-hooks.mjs", ".simple-git-hooks.json", "simple-git-hooks.json"];
905
- var HUSKY_DIR = ".husky";
906
- var GIT_CONFIG = ".git/config";
907
- var HOOK_SCRIPTS = ["precommit", "pre-commit", "prepush", "pre-push"];
908
- function read(root, file) {
909
- try {
910
- return readFileSync9(path10.join(root, file), "utf8");
911
- } catch {
912
- return null;
1162
+ // src/model/path.ts
1163
+ var CHAIN_STAGES = ["enforcement", "verification"];
1164
+ function firstStop(claimId, stages) {
1165
+ for (const stage of CHAIN_STAGES) {
1166
+ const finding = stages[stage];
1167
+ if (finding.state !== "held")
1168
+ return { claimId, stage, ...finding };
913
1169
  }
1170
+ return null;
914
1171
  }
915
- function huskyHooks(root) {
916
- const directory = path10.join(root, HUSKY_DIR);
917
- if (!existsSync9(directory) || !statSync2(directory).isDirectory())
918
- return [];
919
- return readdirSync4(directory).filter((entry) => !entry.startsWith("_") && !entry.startsWith(".")).sort().map((entry) => `${HUSKY_DIR}/${entry}`);
920
- }
921
- function managerFiles(root, packageJson) {
922
- const files = [...huskyHooks(root)];
923
- files.push(...[...LEFTHOOK_FILES, ...SIMPLE_GIT_HOOKS_FILES].filter((file) => existsSync9(path10.join(root, file))));
924
- if (packageJson != null && "simple-git-hooks" in packageJson)
925
- files.push("package.json (simple-git-hooks)");
926
- const gitConfig = read(root, GIT_CONFIG);
927
- if (gitConfig != null && gitConfig.includes("hooksPath"))
928
- files.push(`${GIT_CONFIG} (core.hooksPath)`);
929
- return files;
930
- }
931
- function readHookFacts(root, packageJson, scripts, commandForms) {
932
- const files = managerFiles(root, packageJson);
933
- const script = HOOK_SCRIPTS.find((name) => scripts[name] != null) ?? null;
934
- const sources = files.map((file) => {
935
- if (file.startsWith("package.json"))
936
- return JSON.stringify(packageJson?.["simple-git-hooks"] ?? "");
937
- return read(root, file.split(" ")[0]) ?? "";
938
- });
939
- const manager = files[0] ?? null;
940
- const scriptRunsHarness = script != null && runsHarnessCommand(scripts[script] ?? "", commandForms);
941
- const installed = sources.some((source) => runsHarnessCommand(source, commandForms) || scriptRunsHarness && script != null && source.includes(script));
942
- return { manager, script, runsHarness: manager != null && installed };
943
- }
944
-
945
- // src/commands/doctor/workflows.ts
946
- import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync10 } from "fs";
947
- import path11 from "path";
948
- var WORKFLOWS_DIR = ".github/workflows";
949
- var RUN_STEP = /(?:^|\s)run:[ \t]*(\S.*)$/;
950
- function runStepTexts(source) {
951
- const lines = source.split("\n");
952
- const steps = [];
953
- for (let index = 0; index < lines.length; index += 1) {
954
- const inline = RUN_STEP.exec(lines[index])?.[1];
955
- if (inline == null)
956
- continue;
957
- if (!inline.startsWith("|") && !inline.startsWith(">")) {
958
- steps.push(inline.trim());
1172
+ function selectPath(model, derived) {
1173
+ let selected = null;
1174
+ let selectedDepth = CHAIN_STAGES.length;
1175
+ for (const claim of model.claims) {
1176
+ const stages = derived.claims[claim.id];
1177
+ if (stages === void 0)
959
1178
  continue;
960
- }
961
- const indent = lines[index].length - lines[index].trimStart().length;
962
- const block = [];
963
- for (let next = index + 1; next < lines.length; next += 1) {
964
- const line = lines[next];
965
- if (line.trim() !== "" && line.length - line.trimStart().length <= indent)
966
- break;
967
- block.push(line.trim());
968
- }
969
- steps.push(block.join("\n"));
970
- }
971
- return steps;
972
- }
973
- function readWorkflowFacts(root, commandForms) {
974
- const directory = path11.join(root, WORKFLOWS_DIR);
975
- if (!existsSync10(directory))
976
- return { directory: WORKFLOWS_DIR, files: [], harnessWorkflow: null, unreadable: [] };
977
- const files = readdirSync5(directory).filter((file) => file.endsWith(".yml") || file.endsWith(".yaml")).sort();
978
- const unreadable = [];
979
- let harnessWorkflow = null;
980
- for (const file of files) {
981
- let source;
982
- try {
983
- source = readFileSync10(path11.join(directory, file), "utf8");
984
- } catch {
985
- unreadable.push(`${WORKFLOWS_DIR}/${file}`);
1179
+ const stop = firstStop(claim.id, stages);
1180
+ if (stop === null)
986
1181
  continue;
1182
+ const depth = CHAIN_STAGES.indexOf(stop.stage);
1183
+ if (depth < selectedDepth) {
1184
+ selected = stop;
1185
+ selectedDepth = depth;
987
1186
  }
988
- if (harnessWorkflow == null && runStepTexts(source).some((step) => runsHarnessCommand(step, commandForms)))
989
- harnessWorkflow = `${WORKFLOWS_DIR}/${file}`;
990
1187
  }
991
- return { directory: WORKFLOWS_DIR, files, harnessWorkflow, unreadable };
1188
+ return selected;
992
1189
  }
993
1190
 
994
- // src/commands/doctor/evidence.ts
995
- var TEST_FILE = /\.test\.[cm]?[jt]s$/;
996
- var LINT_POLICY_MARKERS = ["calculateConfigForFile", "lintText", "lintFiles"];
997
- function recordedTestFiles(manifest) {
998
- return Object.keys(manifest.files).filter((file) => TEST_FILE.test(file)).sort();
999
- }
1000
- function gatherEvidence(root, manifest) {
1001
- const harness = readHarnessFacts(root, manifest.harness.command);
1002
- const runner = readRunnerFacts(root, harness.resolved);
1003
- const workflows = readWorkflowFacts(root, harness.commandForms);
1004
- const hooks = readHookFacts(root, harness.packageJson, harness.scripts, harness.commandForms);
1005
- const recordedTests = recordedTestFiles(manifest);
1006
- const policyTests = [];
1007
- const unreadable = [...workflows.unreadable];
1008
- for (const file of recordedTests) {
1009
- try {
1010
- const source = readFileSync11(path12.join(root, file), "utf8");
1011
- if (LINT_POLICY_MARKERS.some((marker) => source.includes(marker)))
1012
- policyTests.push(file);
1013
- } catch {
1014
- unreadable.push(file);
1015
- }
1016
- }
1017
- return { harness, runner, workflows, hooks, recordedTests, policyTests, unreadable };
1191
+ // src/model/state.ts
1192
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
1193
+ import path11 from "path";
1194
+ function evaluateFact(fact, root) {
1195
+ const target = path11.join(root, fact.path);
1196
+ try {
1197
+ if (!existsSync9(target))
1198
+ return "does-not-hold";
1199
+ if (fact.kind === "file-exists")
1200
+ return "holds";
1201
+ return readFileSync7(target, "utf8").includes(fact.needle ?? "") ? "holds" : "does-not-hold";
1202
+ } catch {
1203
+ return "unevaluable";
1204
+ }
1205
+ }
1206
+ function evaluateFacts(model, root) {
1207
+ return Object.fromEntries(model.facts.map((fact) => [fact.id, evaluateFact(fact, root)]));
1208
+ }
1209
+ function pathsOf(outcomes, evaluation) {
1210
+ return [...new Set(outcomes.filter((fact) => fact.evaluation === evaluation).map((fact) => fact.path))];
1211
+ }
1212
+ function resolveFinding(facts, supportedBy2, evaluations) {
1213
+ if (supportedBy2.length === 0)
1214
+ return { state: "unknown", reason: "no-fact-named" };
1215
+ const outcomes = factOutcomes(facts, supportedBy2, evaluations);
1216
+ const [firstUnevaluable, ...restUnevaluable] = pathsOf(outcomes, "unevaluable");
1217
+ if (firstUnevaluable !== void 0)
1218
+ return { state: "unknown", reason: "unevaluable", unevaluable: [firstUnevaluable, ...restUnevaluable] };
1219
+ const [firstDoesNotHold, ...restDoesNotHold] = pathsOf(outcomes, "does-not-hold");
1220
+ if (firstDoesNotHold === void 0)
1221
+ return { state: "held" };
1222
+ return { state: "unsupported", doesNotHold: [firstDoesNotHold, ...restDoesNotHold] };
1223
+ }
1224
+ function resolveState(supportedBy2, evaluations) {
1225
+ return resolveFinding([], supportedBy2, evaluations).state;
1226
+ }
1227
+ function factOutcomes(facts, supportedBy2, evaluations) {
1228
+ return supportedBy2.flatMap((id) => {
1229
+ const evaluation = evaluations[id] ?? "unevaluable";
1230
+ if (evaluation === "holds")
1231
+ return [];
1232
+ return [{ path: facts.find((fact) => fact.id === id)?.path ?? id, evaluation }];
1233
+ });
1234
+ }
1235
+ function deriveModelState(model, root) {
1236
+ const facts = evaluateFacts(model, root);
1237
+ return {
1238
+ facts,
1239
+ hypotheses: Object.fromEntries(model.hypotheses.map((hypothesis) => [hypothesis.id, resolveState(hypothesis.supportedBy, facts)])),
1240
+ claims: Object.fromEntries(model.claims.map((claim) => [claim.id, {
1241
+ enforcement: resolveFinding(model.facts, claim.enforcement?.supportedBy ?? [], facts),
1242
+ verification: resolveFinding(model.facts, claim.verification?.supportedBy ?? [], facts)
1243
+ }]))
1244
+ };
1245
+ }
1246
+
1247
+ // src/commands/doctor/projection.ts
1248
+ function verdict(claim, finding, owner) {
1249
+ return {
1250
+ id: claim.checkId ?? claim.id,
1251
+ claimId: claim.id,
1252
+ level: claim.enforcement?.level ?? "L0",
1253
+ authoredBy: owner(claim),
1254
+ mechanism: claim.enforcement?.mechanism ?? claim.statement,
1255
+ ...finding
1256
+ };
1257
+ }
1258
+ var NOTHING_NAMED = { state: "unknown", reason: "no-fact-named" };
1259
+ function placeClaim(model, derived) {
1260
+ if (model.claims.length === 0)
1261
+ return { at: "no-claim" };
1262
+ const stop = selectPath(model, derived);
1263
+ return stop == null ? { at: "no-stop" } : { at: "stop", stop };
1264
+ }
1265
+ function projectKnowledge(model, root, owner = authoredByOwner) {
1266
+ if (model == null)
1267
+ return { checks: [], youAreHere: { at: "no-model" } };
1268
+ const derived = deriveModelState(model, root);
1269
+ const checks = model.claims.map((claim) => verdict(claim, derived.claims[claim.id]?.enforcement ?? NOTHING_NAMED, owner));
1270
+ return { checks, youAreHere: placeClaim(model, derived) };
1018
1271
  }
1019
1272
 
1020
1273
  // src/commands/doctor/provenance.ts
@@ -1023,18 +1276,18 @@ function markerAuthorship(recorded, body) {
1023
1276
  return "unknown";
1024
1277
  return sha256(body) === recorded.sha ? "construct" : "owner";
1025
1278
  }
1026
- function discoveryProvenance(root, manifest) {
1279
+ function discoveryProvenance(root, manifest, readings = new FileReadings(root)) {
1027
1280
  return DISCOVERY_MARKERS.map((marker) => {
1028
1281
  const recorded = manifest.discovery.markers[marker];
1029
1282
  return {
1030
1283
  marker,
1031
1284
  file: recorded.file,
1032
- authorship: markerAuthorship(recorded, markerBody(root, marker, markerFileFor(root, manifest, marker)))
1285
+ authorship: markerAuthorship(recorded, markerBody(root, marker, markerFileFor(root, manifest, marker), readings))
1033
1286
  };
1034
1287
  });
1035
1288
  }
1036
- function constructAuthored(readings) {
1037
- return readings.filter((reading) => reading.authorship === "construct");
1289
+ function constructAuthored(markers) {
1290
+ return markers.filter((reading2) => reading2.authorship === "construct");
1038
1291
  }
1039
1292
 
1040
1293
  // src/commands/doctor/typecheck.ts
@@ -1044,33 +1297,123 @@ var CAVEATS = {
1044
1297
  text: "node-frontend: `tsc --noEmit` does not see `.vue` or `.svelte` single-file components, so a Vite app that adds them needs the framework's own checker (vue-tsc, svelte-check) in the harness"
1045
1298
  }
1046
1299
  };
1047
- function typecheckWarnings(preset, evidence) {
1300
+ function typecheckWarnings(preset, harness) {
1048
1301
  const caveat = CAVEATS[preset];
1049
- if (caveat == null || caveat.checkers.some((checker) => evidence.harness.resolved.includes(checker)))
1302
+ if (caveat == null || caveat.checkers.some((checker) => harness.resolved.includes(checker)))
1050
1303
  return [];
1051
1304
  return [caveat.text];
1052
1305
  }
1053
1306
 
1054
- // src/commands/doctor/verdict.ts
1055
- var CHECK_IDS = ["lint-policy", "construct-tests", "ci", "hook", "red-gate"];
1056
- var LEVELS = ["L0", "L1", "L2", "L3", "L4"];
1057
- function weakestLink(checks) {
1058
- const claimed = checks.filter((check) => check.state === "present");
1059
- if (claimed.length === 0)
1060
- return null;
1061
- const ordered = [...claimed].sort((left, right) => CHECK_IDS.indexOf(left.id) - CHECK_IDS.indexOf(right.id));
1062
- const weakest = ordered.reduce((current, check) => LEVELS.indexOf(check.level) < LEVELS.indexOf(current.level) ? check : current);
1063
- return { id: weakest.id, level: weakest.level };
1307
+ // src/commands/doctor/runner.ts
1308
+ var RUNNER_CONFIG_FILES = [
1309
+ "vitest.config.ts",
1310
+ "vitest.config.mts",
1311
+ "vitest.config.js",
1312
+ "vitest.config.mjs",
1313
+ "vite.config.ts",
1314
+ "vite.config.mts",
1315
+ "vite.config.js",
1316
+ "vite.config.mjs"
1317
+ ];
1318
+ var STRING_LITERAL = /^(['"])(.*)\1$/;
1319
+ function literalEntries(body) {
1320
+ const trimmed = body.trim();
1321
+ if (trimmed === "")
1322
+ return [];
1323
+ const entries = trimmed.split(",").map((entry) => entry.trim()).filter((entry) => entry !== "");
1324
+ const values = entries.map((entry) => STRING_LITERAL.exec(entry)?.[2]);
1325
+ return values.every((value) => value != null) ? values : null;
1326
+ }
1327
+ function includeGlobs(source) {
1328
+ const found = [];
1329
+ let literal = false;
1330
+ for (const match of source.matchAll(/include\s*:\s*/g)) {
1331
+ const rest = source.slice(match.index + match[0].length);
1332
+ if (!rest.startsWith("["))
1333
+ return null;
1334
+ const close = rest.indexOf("]");
1335
+ if (close === -1)
1336
+ return null;
1337
+ const entries = literalEntries(rest.slice(1, close));
1338
+ if (entries == null)
1339
+ return null;
1340
+ literal = true;
1341
+ found.push(...entries);
1342
+ }
1343
+ return literal ? found : null;
1344
+ }
1345
+ function escapeLiteral(character) {
1346
+ return /[.+^${}()|[\]\\]/.test(character) ? `\\${character}` : character;
1347
+ }
1348
+ function globToRegExp(glob) {
1349
+ let pattern = "";
1350
+ let braces = 0;
1351
+ for (let index = 0; index < glob.length; index += 1) {
1352
+ const character = glob[index];
1353
+ if (character === "*" && glob[index + 1] === "*" && glob[index + 2] === "/") {
1354
+ pattern += "(?:[^/]+/)*";
1355
+ index += 2;
1356
+ continue;
1357
+ }
1358
+ if (character === "*" && glob[index + 1] === "*") {
1359
+ pattern += ".*";
1360
+ index += 1;
1361
+ continue;
1362
+ }
1363
+ if (character === "*") {
1364
+ pattern += "[^/]*";
1365
+ continue;
1366
+ }
1367
+ if (character === "?") {
1368
+ pattern += "[^/]";
1369
+ continue;
1370
+ }
1371
+ if (character === "{") {
1372
+ braces += 1;
1373
+ pattern += "(?:";
1374
+ continue;
1375
+ }
1376
+ if (character === "}" && braces > 0) {
1377
+ braces -= 1;
1378
+ pattern += ")";
1379
+ continue;
1380
+ }
1381
+ if (character === "," && braces > 0) {
1382
+ pattern += "|";
1383
+ continue;
1384
+ }
1385
+ pattern += escapeLiteral(character);
1386
+ }
1387
+ return new RegExp(`^${pattern}$`);
1388
+ }
1389
+ function matchesAnyGlob(file, globs) {
1390
+ const normalized = file.replace(/^\.\//, "");
1391
+ return globs.some((glob) => globToRegExp(glob.replace(/^\.\//, "")).test(normalized));
1392
+ }
1393
+
1394
+ // src/commands/doctor/uncollected-tests.ts
1395
+ var TEST_FILE = /\.test\.[cm]?[jt]s$/;
1396
+ function uncollectedTests(root, manifest, readings = new FileReadings(root)) {
1397
+ const config = RUNNER_CONFIG_FILES.find((candidate) => manifest.files[candidate] != null);
1398
+ if (config == null)
1399
+ return [];
1400
+ const source = readings.read(config);
1401
+ if (source == null)
1402
+ return [];
1403
+ const globs = includeGlobs(source);
1404
+ if (globs == null)
1405
+ return [];
1406
+ return Object.keys(manifest.files).filter((file) => TEST_FILE.test(file) && !matchesAnyGlob(file, globs)).sort();
1064
1407
  }
1065
1408
 
1066
1409
  // src/sync/replay.ts
1067
- import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
1410
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
1068
1411
  import { tmpdir } from "os";
1069
- import path15 from "path";
1412
+ import path14 from "path";
1070
1413
 
1071
1414
  // src/materialize/plan.ts
1072
- import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
1073
- import path14 from "path";
1415
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
1416
+ import path13 from "path";
1074
1417
 
1075
1418
  // src/materialize/rules.ts
1076
1419
  var CLAUDE_RULES_DIR = ".claude/rules/";
@@ -1249,43 +1592,43 @@ ${end}
1249
1592
  }
1250
1593
 
1251
1594
  // src/materialize/templates.ts
1252
- import { existsSync as existsSync11, readdirSync as readdirSync6, statSync as statSync3 } from "fs";
1253
- import path13 from "path";
1595
+ import { existsSync as existsSync10, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
1596
+ import path12 from "path";
1254
1597
  import { fileURLToPath as fileURLToPath2 } from "url";
1255
- var HERE2 = path13.dirname(fileURLToPath2(import.meta.url));
1598
+ var HERE2 = path12.dirname(fileURLToPath2(import.meta.url));
1256
1599
  function templatesRoot() {
1257
- const candidates = [path13.resolve(HERE2, "../templates"), path13.resolve(HERE2, "../../templates")];
1258
- const found = candidates.find((candidate) => existsSync11(candidate));
1600
+ const candidates = [path12.resolve(HERE2, "../templates"), path12.resolve(HERE2, "../../templates")];
1601
+ const found = candidates.find((candidate) => existsSync10(candidate));
1259
1602
  if (found == null)
1260
1603
  throw new Error(`templates directory not found next to ${HERE2}`);
1261
1604
  return found;
1262
1605
  }
1263
1606
  var EXISTING_SUFFIX = ".existing.eta";
1264
1607
  function toTargetPath(relative) {
1265
- const segments = relative.split(path13.sep).map((segment) => segment.startsWith("_") ? `.${segment.slice(1)}` : segment);
1608
+ const segments = relative.split(path12.sep).map((segment) => segment.startsWith("_") ? `.${segment.slice(1)}` : segment);
1266
1609
  const joined = segments.join("/");
1267
1610
  if (joined.endsWith(EXISTING_SUFFIX))
1268
1611
  return { target: joined.slice(0, -EXISTING_SUFFIX.length), rendered: true, variant: "existing" };
1269
1612
  return joined.endsWith(".eta") ? { target: joined.slice(0, -".eta".length), rendered: true, variant: "default" } : { target: joined, rendered: false, variant: "default" };
1270
1613
  }
1271
1614
  function walk(root, current, files) {
1272
- for (const entry of readdirSync6(current).sort()) {
1273
- const absolute = path13.join(current, entry);
1274
- if (statSync3(absolute).isDirectory())
1615
+ for (const entry of readdirSync4(current).sort()) {
1616
+ const absolute = path12.join(current, entry);
1617
+ if (statSync2(absolute).isDirectory())
1275
1618
  walk(root, absolute, files);
1276
1619
  else if (entry !== ".DS_Store")
1277
- files.push(path13.relative(root, absolute));
1620
+ files.push(path12.relative(root, absolute));
1278
1621
  }
1279
1622
  }
1280
1623
  function listTemplateFiles(group) {
1281
- const root = path13.join(templatesRoot(), group);
1282
- if (!existsSync11(root))
1624
+ const root = path12.join(templatesRoot(), group);
1625
+ if (!existsSync10(root))
1283
1626
  throw new Error(`template group "${group}" does not exist`);
1284
1627
  const files = [];
1285
1628
  walk(root, root, files);
1286
1629
  return files.map((relative) => {
1287
1630
  const { target, rendered, variant } = toTargetPath(relative);
1288
- return { group, source: path13.join(root, relative), target, rendered, variant };
1631
+ return { group, source: path12.join(root, relative), target, rendered, variant };
1289
1632
  });
1290
1633
  }
1291
1634
  var BLOCK = /^[ \t]*\{\{#(if|unless) (\w+)\}\}[ \t]*\n([\s\S]*?)^[ \t]*\{\{\/\1\}\}[ \t]*\n/gm;
@@ -1312,7 +1655,7 @@ function mountTarget(mount, target) {
1312
1655
  return mount.into == null || mount.into === "." ? target : `${mount.into.replace(/\/$/, "")}/${target}`;
1313
1656
  }
1314
1657
  function readTemplate(source, rendered, vars) {
1315
- const raw = readFileSync12(source, "utf8");
1658
+ const raw = readFileSync8(source, "utf8");
1316
1659
  return rendered ? render(raw, vars) : raw;
1317
1660
  }
1318
1661
  var SORTED_SECTIONS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
@@ -1370,13 +1713,13 @@ function layerJson(earlier, later) {
1370
1713
  var NOT_ADDED_TO_EXISTING_MANIFEST = ["version"];
1371
1714
  function planOne(root, target, content, conflicts, existingVariant) {
1372
1715
  const strategy = strategyFor(target);
1373
- const absolute = path14.join(root, target);
1374
- const exists = existsSync12(absolute);
1716
+ const absolute = path13.join(root, target);
1717
+ const exists = existsSync11(absolute);
1375
1718
  if (!exists) {
1376
1719
  return strategy === "append-block" ? { target, strategy, action: "create", content: appendBlock("", content, target), variant: "default" } : { target, strategy, action: "create", content };
1377
1720
  }
1378
1721
  if (strategy === "merge-json") {
1379
- const existing = JSON.parse(readFileSync12(absolute, "utf8"));
1722
+ const existing = JSON.parse(readFileSync8(absolute, "utf8"));
1380
1723
  const incoming = JSON.parse(content);
1381
1724
  for (const key of NOT_ADDED_TO_EXISTING_MANIFEST)
1382
1725
  delete incoming[key];
@@ -1387,7 +1730,7 @@ function planOne(root, target, content, conflicts, existingVariant) {
1387
1730
  ` };
1388
1731
  }
1389
1732
  if (strategy === "append-block") {
1390
- const existing = readFileSync12(absolute, "utf8");
1733
+ const existing = readFileSync8(absolute, "utf8");
1391
1734
  return {
1392
1735
  target,
1393
1736
  strategy,
@@ -1546,6 +1889,9 @@ function isPresetId(value) {
1546
1889
  function getPreset(id) {
1547
1890
  return PRESETS[id];
1548
1891
  }
1892
+ function sampleGroups(preset) {
1893
+ return preset.groups.map((group) => typeof group === "string" ? group : group.group).filter((group) => group.endsWith("/sample"));
1894
+ }
1549
1895
  function aiGroups(target) {
1550
1896
  return target === "both" ? ["ai/shared", "ai/claude", "ai/cursor"] : ["ai/shared", `ai/${target}`];
1551
1897
  }
@@ -1728,7 +2074,7 @@ function establishVariant(input) {
1728
2074
  }
1729
2075
 
1730
2076
  // src/sync/replay.ts
1731
- var NO_TREE_TO_PLAN_AGAINST = path15.join(tmpdir(), "mikoshi-construct-replay-renders-against-no-tree");
2077
+ var NO_TREE_TO_PLAN_AGAINST = path14.join(tmpdir(), "mikoshi-construct-replay-renders-against-no-tree");
1732
2078
  function replayedGroups(manifest) {
1733
2079
  const preset = getPreset(manifest.preset);
1734
2080
  return [...preset.groups, ...aiGroups(manifest.ai), ...reviewGroups(manifest.review?.provider ?? "none")];
@@ -1801,9 +2147,9 @@ function producedInTheVariantThatWroteIt(templates, variants) {
1801
2147
  function presentInTree(root, targets) {
1802
2148
  const present = {};
1803
2149
  for (const target of new Set(targets)) {
1804
- const absolute = path15.join(root, target);
1805
- if (existsSync13(absolute))
1806
- present[target] = readFileSync13(absolute, "utf8");
2150
+ const absolute = path14.join(root, target);
2151
+ if (existsSync12(absolute))
2152
+ present[target] = readFileSync9(absolute, "utf8");
1807
2153
  }
1808
2154
  return present;
1809
2155
  }
@@ -1878,14 +2224,39 @@ function versionGap(root, manifest, version) {
1878
2224
  }
1879
2225
 
1880
2226
  // src/commands/doctor/report.ts
1881
- function checkLine(ui2, check) {
1882
- return ` ${check.id.padEnd(16)} ${check.level} ${check.state.padEnd(8)} ${ui2.theme.dim(check.evidence)}`;
2227
+ function reading(ui2, check) {
2228
+ switch (check.state) {
2229
+ case "held":
2230
+ return ui2.lore.verdictHeld(check.mechanism);
2231
+ case "unsupported":
2232
+ return ui2.lore.verdictUnsupported(check.mechanism, check.doesNotHold);
2233
+ case "unknown":
2234
+ return check.reason === "unevaluable" ? ui2.lore.verdictUnevaluable(check.mechanism, check.unevaluable) : ui2.lore.verdictNothingNamed(check.mechanism);
2235
+ }
2236
+ }
2237
+ function checkLine(ui2, check, width) {
2238
+ return ` ${check.id.padEnd(width)} ${check.level} ${check.state.padEnd(12)} ${ui2.theme.dim(reading(ui2, check))}`;
2239
+ }
2240
+ function claimsRead(ui2, placement) {
2241
+ switch (placement.at) {
2242
+ case "no-model":
2243
+ return ui2.lore.enforcementNoModel;
2244
+ case "no-claim":
2245
+ return ui2.lore.enforcementNoClaim;
2246
+ default:
2247
+ return null;
2248
+ }
1883
2249
  }
1884
- function printChecks(ui2, checks) {
2250
+ function printChecks(ui2, checks, placement) {
2251
+ const width = Math.max(16, ...checks.map((check) => check.id.length));
1885
2252
  ui2.line();
1886
2253
  ui2.line(ui2.theme.accent(ui2.lore.enforcement));
2254
+ const read = claimsRead(ui2, placement);
2255
+ if (read != null)
2256
+ ui2.line(ui2.theme.dim(` ${read}`));
1887
2257
  for (const check of checks)
1888
- ui2.line(checkLine(ui2, check));
2258
+ ui2.line(checkLine(ui2, check, width));
2259
+ ui2.line(ui2.theme.dim(` ${ui2.lore.executesNothing}`));
1889
2260
  }
1890
2261
  function printProvenance(ui2, provenance) {
1891
2262
  const authored = constructAuthored(provenance);
@@ -1893,8 +2264,8 @@ function printProvenance(ui2, provenance) {
1893
2264
  return;
1894
2265
  ui2.line();
1895
2266
  ui2.line(ui2.theme.accent(ui2.lore.provenance));
1896
- for (const reading of authored)
1897
- ui2.line(` ${reading.marker.padEnd(20)} ${ui2.theme.dim(reading.file)}`);
2267
+ for (const reading2 of authored)
2268
+ ui2.line(` ${reading2.marker.padEnd(20)} ${ui2.theme.dim(reading2.file)}`);
1898
2269
  ui2.line(ui2.theme.dim(` ${ui2.lore.stillConstructAuthored(authored.length)}`));
1899
2270
  }
1900
2271
  function gapReading(ui2, gap) {
@@ -1906,14 +2277,29 @@ function printVersionGap(ui2, gap) {
1906
2277
  ui2.line(ui2.theme.dim(` ${ui2.lore.syncVersionGap(gap.materializedBy, gap.readBy)}`));
1907
2278
  ui2.line(ui2.theme.dim(` ${gapReading(ui2, gap)}`));
1908
2279
  }
1909
- function printWeakestLink(ui2, weakest) {
1910
- ui2.line();
1911
- if (weakest == null) {
1912
- ui2.line(ui2.theme.bold(ui2.lore.weakestLinkNone));
1913
- return;
2280
+ function stoppedLine(ui2, stop) {
2281
+ switch (stop.state) {
2282
+ case "unsupported":
2283
+ return ui2.lore.youAreHereUnsupported(stop.claimId, stop.stage, stop.doesNotHold);
2284
+ case "unknown":
2285
+ return stop.reason === "unevaluable" ? ui2.lore.youAreHereUnevaluable(stop.claimId, stop.stage, stop.unevaluable) : ui2.lore.youAreHereNothingNamed(stop.claimId, stop.stage);
2286
+ }
2287
+ }
2288
+ function placementLine(ui2, placement) {
2289
+ switch (placement.at) {
2290
+ case "stop":
2291
+ return stoppedLine(ui2, placement.stop);
2292
+ case "no-stop":
2293
+ return ui2.lore.youAreHereNone;
2294
+ case "no-claim":
2295
+ return ui2.lore.youAreHereNoClaim;
2296
+ case "no-model":
2297
+ return ui2.lore.youAreHereNoModel;
1914
2298
  }
1915
- const lift = ui2.lore.levelLift[weakest.level];
1916
- ui2.line(`${ui2.theme.bold(ui2.lore.weakestLink(weakest.id, weakest.level))}${lift == null ? "" : ui2.theme.dim(` \u2014 ${lift}`)}`);
2299
+ }
2300
+ function printYouAreHere(ui2, placement) {
2301
+ ui2.line();
2302
+ ui2.line(ui2.theme.bold(placementLine(ui2, placement)));
1917
2303
  }
1918
2304
  function printDoctor(ui2, result) {
1919
2305
  if (result == null) {
@@ -1922,6 +2308,10 @@ function printDoctor(ui2, result) {
1922
2308
  }
1923
2309
  if (result.harnessProblems.length > 0)
1924
2310
  ui2.glitch("Harness is broken.", result.harnessProblems);
2311
+ if (result.uncollectedTests.length > 0)
2312
+ ui2.glitch(ui2.lore.uncollectedTests, result.uncollectedTests);
2313
+ if (result.unreadableFiles.length > 0)
2314
+ ui2.glitch(ui2.lore.unreadableFiles, result.unreadableFiles);
1925
2315
  if (result.missingFiles.length > 0)
1926
2316
  ui2.glitch("Baseline files are missing.", result.missingFiles);
1927
2317
  if (result.missingDiscovery.length > 0)
@@ -1934,8 +2324,8 @@ function printDoctor(ui2, result) {
1934
2324
  if (result.ok)
1935
2325
  ui2.ok(ui2.lore.stable);
1936
2326
  printProvenance(ui2, result.provenance);
1937
- printChecks(ui2, result.checks);
1938
- printWeakestLink(ui2, result.weakestLink);
2327
+ printChecks(ui2, result.checks, result.youAreHere);
2328
+ printYouAreHere(ui2, result.youAreHere);
1939
2329
  return result.ok ? 0 : 1;
1940
2330
  }
1941
2331
 
@@ -1944,54 +2334,66 @@ function runDoctor(root, version = VERSION) {
1944
2334
  const manifest = readManifest(root);
1945
2335
  if (manifest == null)
1946
2336
  return null;
1947
- const evidence = gatherEvidence(root, manifest);
1948
- const baseline = baselineVerdict(root, manifest);
1949
- const problems = harnessProblems(root, manifest, evidence.harness);
1950
- const checks = [
1951
- lintPolicyCheck(evidence),
1952
- constructTestsCheck(evidence),
1953
- ciCheck(evidence),
1954
- hookCheck(evidence),
1955
- redGateCheck(evidence)
1956
- ];
2337
+ const readings = new FileReadings(root);
2338
+ const harness = readHarnessFacts(root, manifest.harness.command, readings);
2339
+ const baseline = baselineVerdict(root, manifest, readings);
2340
+ const problems = harnessProblems(root, manifest, harness, readings);
2341
+ const markers = missingDiscovery(root, manifest, readings);
2342
+ const provenance = discoveryProvenance(root, manifest, readings);
2343
+ const uncollected = uncollectedTests(root, manifest, readings);
2344
+ const knowledge = projectKnowledge(readModel(root), root);
2345
+ const unreadableFiles = readings.files;
2346
+ const intact = {
2347
+ missingFiles: baseline.missingFiles,
2348
+ modifiedFiles: baseline.modifiedFiles,
2349
+ unreadableFiles,
2350
+ missingDiscovery: markers,
2351
+ provenance,
2352
+ harnessProblems: problems,
2353
+ uncollectedTests: uncollected,
2354
+ warnings: typecheckWarnings(manifest.preset, harness),
2355
+ versionGap: versionGap(root, manifest, version)
2356
+ };
1957
2357
  return {
1958
- ok: baseline.missingFiles.length === 0 && problems.length === 0,
2358
+ ok: isIntact(intact),
1959
2359
  missingFiles: baseline.missingFiles,
1960
2360
  modifiedFiles: baseline.modifiedFiles,
1961
- missingDiscovery: missingDiscovery(root, manifest),
1962
- provenance: discoveryProvenance(root, manifest),
2361
+ unreadableFiles,
2362
+ missingDiscovery: markers,
2363
+ provenance,
1963
2364
  harnessProblems: problems,
1964
- warnings: typecheckWarnings(manifest.preset, evidence),
1965
- checks,
1966
- weakestLink: weakestLink(checks),
2365
+ uncollectedTests: uncollected,
2366
+ warnings: typecheckWarnings(manifest.preset, harness),
2367
+ checks: knowledge.checks,
2368
+ youAreHere: knowledge.youAreHere,
1967
2369
  versionGap: versionGap(root, manifest, version)
1968
2370
  };
1969
2371
  }
1970
2372
 
1971
2373
  // src/commands/init.ts
1972
2374
  import { mkdirSync as mkdirSync2 } from "fs";
1973
- import path21 from "path";
2375
+ import path20 from "path";
1974
2376
 
1975
2377
  // src/detect/index.ts
1976
- import { existsSync as existsSync17 } from "fs";
1977
- import path19 from "path";
2378
+ import { existsSync as existsSync16 } from "fs";
2379
+ import path18 from "path";
1978
2380
  import process3 from "process";
1979
2381
 
1980
2382
  // src/detect/layout.ts
1981
- import { existsSync as existsSync15, readdirSync as readdirSync7, readFileSync as readFileSync15, statSync as statSync4 } from "fs";
1982
- import path17 from "path";
2383
+ import { existsSync as existsSync14, readdirSync as readdirSync5, readFileSync as readFileSync11, statSync as statSync3 } from "fs";
2384
+ import path16 from "path";
1983
2385
 
1984
2386
  // src/detect/workspaces.ts
1985
- import { existsSync as existsSync14, readFileSync as readFileSync14 } from "fs";
1986
- import path16 from "path";
2387
+ import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
2388
+ import path15 from "path";
1987
2389
  var TOP_LEVEL_PACKAGES_KEY = /^packages:(.*)$/m;
1988
2390
  var EMPTY_FLOW_SEQUENCE = /^\[\s*\]$/;
1989
2391
  var BLOCK_SEQUENCE_ENTRY = /^[ \t]+-[ \t]*\S/;
1990
2392
  function readIfPresent(file) {
1991
- if (!existsSync14(file))
2393
+ if (!existsSync13(file))
1992
2394
  return null;
1993
2395
  try {
1994
- return readFileSync14(file, "utf8");
2396
+ return readFileSync10(file, "utf8");
1995
2397
  } catch {
1996
2398
  return null;
1997
2399
  }
@@ -2005,7 +2407,7 @@ function startsBlockSequence(rest) {
2005
2407
  return false;
2006
2408
  }
2007
2409
  function declaresPnpmPackages(dir) {
2008
- const content = readIfPresent(path16.join(dir, "pnpm-workspace.yaml"));
2410
+ const content = readIfPresent(path15.join(dir, "pnpm-workspace.yaml"));
2009
2411
  if (content == null)
2010
2412
  return false;
2011
2413
  const match = TOP_LEVEL_PACKAGES_KEY.exec(content);
@@ -2019,7 +2421,7 @@ function declaresPnpmPackages(dir) {
2019
2421
  return startsBlockSequence(content.slice(match.index + match[0].length));
2020
2422
  }
2021
2423
  function declaresNpmWorkspaces(dir) {
2022
- const content = readIfPresent(path16.join(dir, "package.json"));
2424
+ const content = readIfPresent(path15.join(dir, "package.json"));
2023
2425
  if (content == null)
2024
2426
  return false;
2025
2427
  let workspaces;
@@ -2037,9 +2439,9 @@ function declaresNpmWorkspaces(dir) {
2037
2439
  // src/detect/layout.ts
2038
2440
  var IGNORED_ENTRIES = /* @__PURE__ */ new Set([".git", ".DS_Store", ".gitignore", ".gitattributes", "LICENSE", "README.md", ".idea", ".vscode"]);
2039
2441
  function isEmptyDir(dir) {
2040
- if (!existsSync15(dir))
2442
+ if (!existsSync14(dir))
2041
2443
  return true;
2042
- return readdirSync7(dir).every((entry) => IGNORED_ENTRIES.has(entry));
2444
+ return readdirSync5(dir).every((entry) => IGNORED_ENTRIES.has(entry));
2043
2445
  }
2044
2446
  function detectMonorepoTools(dir) {
2045
2447
  const tools = [];
@@ -2047,40 +2449,40 @@ function detectMonorepoTools(dir) {
2047
2449
  tools.push("pnpm-workspace");
2048
2450
  if (declaresNpmWorkspaces(dir))
2049
2451
  tools.push("npm-workspaces");
2050
- if (existsSync15(path17.join(dir, "turbo.json")))
2452
+ if (existsSync14(path16.join(dir, "turbo.json")))
2051
2453
  tools.push("turbo");
2052
- if (existsSync15(path17.join(dir, "nx.json")))
2454
+ if (existsSync14(path16.join(dir, "nx.json")))
2053
2455
  tools.push("nx");
2054
2456
  return tools;
2055
2457
  }
2056
2458
  function detectWorkspaceDirs(dir) {
2057
- return ["apps", "packages", "libs", "services"].filter((name) => existsSync15(path17.join(dir, name)) && statSync4(path17.join(dir, name)).isDirectory());
2459
+ return ["apps", "packages", "libs", "services"].filter((name) => existsSync14(path16.join(dir, name)) && statSync3(path16.join(dir, name)).isDirectory());
2058
2460
  }
2059
2461
  function packageName(dir) {
2060
2462
  try {
2061
- const parsed = JSON.parse(readFileSync15(path17.join(dir, "package.json"), "utf8"));
2463
+ const parsed = JSON.parse(readFileSync11(path16.join(dir, "package.json"), "utf8"));
2062
2464
  return typeof parsed.name === "string" && parsed.name !== "" ? parsed.name : null;
2063
2465
  } catch {
2064
2466
  return null;
2065
2467
  }
2066
2468
  }
2067
2469
  function detectWorkspacePackages(root, workspaceDirs) {
2068
- return workspaceDirs.flatMap((parent) => readdirSync7(path17.join(root, parent)).sort().map((entry) => `${parent}/${entry}`).filter((dir) => existsSync15(path17.join(root, dir, "package.json"))).map((dir) => ({ dir, name: packageName(path17.join(root, dir)) ?? dir.split("/").at(-1) ?? dir })));
2470
+ return workspaceDirs.flatMap((parent) => readdirSync5(path16.join(root, parent)).sort().map((entry) => `${parent}/${entry}`).filter((dir) => existsSync14(path16.join(root, dir, "package.json"))).map((dir) => ({ dir, name: packageName(path16.join(root, dir)) ?? dir.split("/").at(-1) ?? dir })));
2069
2471
  }
2070
2472
  function detectLayout(dir, monorepoTools, workspaceDirs, hasSrc) {
2071
2473
  if (isEmptyDir(dir))
2072
2474
  return "empty";
2073
2475
  if (monorepoTools.length > 0 || workspaceDirs.length > 0)
2074
2476
  return "monorepo";
2075
- if (hasSrc || existsSync15(path17.join(dir, "package.json")))
2477
+ if (hasSrc || existsSync14(path16.join(dir, "package.json")))
2076
2478
  return "single";
2077
2479
  return "unknown";
2078
2480
  }
2079
2481
 
2080
2482
  // src/detect/package-manager.ts
2081
2483
  import { execFileSync } from "child_process";
2082
- import { existsSync as existsSync16, readFileSync as readFileSync16 } from "fs";
2083
- import path18 from "path";
2484
+ import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
2485
+ import path17 from "path";
2084
2486
  var LOCKFILES = [
2085
2487
  ["pnpm-lock.yaml", "pnpm"],
2086
2488
  ["bun.lockb", "bun"],
@@ -2089,11 +2491,11 @@ var LOCKFILES = [
2089
2491
  ["package-lock.json", "npm"]
2090
2492
  ];
2091
2493
  function fromPackageManagerField(dir) {
2092
- const manifest = path18.join(dir, "package.json");
2093
- if (!existsSync16(manifest))
2494
+ const manifest = path17.join(dir, "package.json");
2495
+ if (!existsSync15(manifest))
2094
2496
  return null;
2095
2497
  try {
2096
- const parsed = JSON.parse(readFileSync16(manifest, "utf8"));
2498
+ const parsed = JSON.parse(readFileSync12(manifest, "utf8"));
2097
2499
  const name = parsed.packageManager?.split("@")[0];
2098
2500
  return name === "pnpm" || name === "npm" || name === "yarn" || name === "bun" ? name : null;
2099
2501
  } catch {
@@ -2105,10 +2507,10 @@ function detectPackageManager(dir) {
2105
2507
  if (declared != null)
2106
2508
  return declared;
2107
2509
  for (const [lockfile, manager] of LOCKFILES) {
2108
- if (existsSync16(path18.join(dir, lockfile)))
2510
+ if (existsSync15(path17.join(dir, lockfile)))
2109
2511
  return manager;
2110
2512
  }
2111
- return existsSync16(path18.join(dir, "package.json")) ? "npm" : "none";
2513
+ return existsSync15(path17.join(dir, "package.json")) ? "npm" : "none";
2112
2514
  }
2113
2515
  var pnpmVersionCache;
2114
2516
  function detectPnpmVersion() {
@@ -2124,10 +2526,10 @@ function detectPnpmVersion() {
2124
2526
 
2125
2527
  // src/detect/index.ts
2126
2528
  function detect(dir) {
2127
- const root = path19.resolve(dir);
2529
+ const root = path18.resolve(dir);
2128
2530
  const monorepoTools = detectMonorepoTools(root);
2129
2531
  const workspaceDirs = detectWorkspaceDirs(root);
2130
- const hasSrc = existsSync17(path19.join(root, "src"));
2532
+ const hasSrc = existsSync16(path18.join(root, "src"));
2131
2533
  return {
2132
2534
  dir: root,
2133
2535
  packageManager: detectPackageManager(root),
@@ -2143,23 +2545,23 @@ function detect(dir) {
2143
2545
  }
2144
2546
 
2145
2547
  // src/materialize/apply.ts
2146
- import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
2147
- import path20 from "path";
2548
+ import { mkdirSync, writeFileSync as writeFileSync3 } from "fs";
2549
+ import path19 from "path";
2148
2550
  function applyPlan(root, ops) {
2149
2551
  const written = [];
2150
2552
  for (const op of ops) {
2151
2553
  if (op.action === "skip")
2152
2554
  continue;
2153
- const absolute = path20.join(root, op.target);
2154
- mkdirSync(path20.dirname(absolute), { recursive: true });
2155
- writeFileSync2(absolute, op.content);
2555
+ const absolute = path19.join(root, op.target);
2556
+ mkdirSync(path19.dirname(absolute), { recursive: true });
2557
+ writeFileSync3(absolute, op.content);
2156
2558
  written.push(op);
2157
2559
  }
2158
2560
  return written;
2159
2561
  }
2160
2562
 
2161
2563
  // src/ui/prompts.ts
2162
- import { cancel, confirm, isCancel, multiselect, select, text } from "@clack/prompts";
2564
+ import { cancel, confirm, isCancel, multiselect, select, text as text2 } from "@clack/prompts";
2163
2565
  var PROJECT_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
2164
2566
  function isValidProjectName(value) {
2165
2567
  return PROJECT_NAME_PATTERN.test(value);
@@ -2210,7 +2612,7 @@ function createClackPrompter(lore, streams = {}) {
2210
2612
  return selected == null ? void 0 : toAiTarget(selected);
2211
2613
  },
2212
2614
  async projectName(initial) {
2213
- const answer = await text({
2615
+ const answer = await text2({
2214
2616
  ...streams,
2215
2617
  message: lore.askName,
2216
2618
  initialValue: initial,
@@ -2325,7 +2727,7 @@ async function askChoices(ui2, options, report, root, prompter) {
2325
2727
  return { presetId, ai, projectName, review };
2326
2728
  }
2327
2729
  async function runInit(ui2, options, prompter) {
2328
- const root = path21.resolve(options.dir);
2730
+ const root = path20.resolve(options.dir);
2329
2731
  mkdirSync2(root, { recursive: true });
2330
2732
  if (!options.yes && prompter == null) {
2331
2733
  ui2.glitch(ui2.lore.needsTerminal);
@@ -2401,10 +2803,19 @@ async function runInit(ui2, options, prompter) {
2401
2803
  }
2402
2804
  if (interactive != null && await interactive.confirm(ui2.lore.confirm) !== true)
2403
2805
  return aborted(skipped, plan.conflicts);
2806
+ const existingModel = readModel(root);
2404
2807
  const written = applyPlan(root, plan.ops);
2405
2808
  const previous = readManifest(root);
2406
2809
  const manifest = buildManifest({ version: VERSION, preset: presetId, ai, review, vars, written, contracts: preset.contracts, previous });
2407
2810
  writeManifest(root, manifest);
2811
+ const samples = sampleGroups(preset);
2812
+ const sample = samples.length > 0 && samples.every((group) => !plan.omittedGroups.includes(group));
2813
+ const merged = mergeModel(existingModel, buildModel({ vars, contracts: preset.contracts, sample }));
2814
+ writeModel(root, merged.model);
2815
+ if (merged.retained.length > 0) {
2816
+ const standingOn = [...new Set(merged.retained.flatMap((fact) => fact.stoodOnBy))];
2817
+ ui2.line(ui2.theme.dim(` ${ui2.lore.recordFactsRetained(merged.retained.map((fact) => fact.id), standingOn)}`));
2818
+ }
2408
2819
  if (previous != null) {
2409
2820
  const carriedOver = Object.keys(previous.files).filter((target) => !written.some((op) => op.target === target)).length;
2410
2821
  const added = written.filter((op) => previous.files[op.target] == null).length;
@@ -2495,8 +2906,8 @@ function printPaths(ui2, report) {
2495
2906
  }
2496
2907
  }
2497
2908
  function printMergedNote(ui2, report) {
2498
- const listed = report.classifications.filter((entry) => LISTED_CLASSES.includes(entry.class));
2499
- if (!listed.some((entry) => entry.strategy === "merge-json"))
2909
+ const listed2 = report.classifications.filter((entry) => LISTED_CLASSES.includes(entry.class));
2910
+ if (!listed2.some((entry) => entry.strategy === "merge-json"))
2500
2911
  return;
2501
2912
  ui2.line();
2502
2913
  ui2.line(ui2.theme.dim(ui2.lore.syncMergedNotWritten));
@@ -2625,6 +3036,9 @@ var BANNER = String.raw`
2625
3036
  ██║╚██╔╝██║██║██╔═██╗ ██║ ██║╚════██║██╔══██║██║
2626
3037
  ██║ ╚═╝ ██║██║██║ ██╗╚██████╔╝███████║██║ ██║██║
2627
3038
  ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝`;
3039
+ function andMore(rest) {
3040
+ return rest.length === 0 ? "" : `, and ${rest.length} more ${rest.length === 1 ? "fact" : "facts"}`;
3041
+ }
2628
3042
  var LORE = {
2629
3043
  subtitle: (version) => `--- CONSTRUCT ENGINE v${version} // ARASAKA SUB-NET ---`,
2630
3044
  johnnyWakeUp: "Wake up, Netrunner. We have a repository to build.",
@@ -2660,15 +3074,21 @@ var LORE = {
2660
3074
  baselineGapUnknown: "What a sync would add or update cannot be established from this manifest: run `construct sync`.",
2661
3075
  enforcement: "ENFORCEMENT TRACE",
2662
3076
  typecheckCaveat: "Typecheck cannot carry this stack alone.",
2663
- weakestLink: (id, level) => `WEAKEST LINK: ${id} at ${level}`,
2664
- levelLift: {
2665
- L0: "wire it to a hook or a workflow step and it climbs",
2666
- L1: "only a reviewer stands behind it; a hook or a workflow step raises it",
2667
- L2: "a hook is bypassable with --no-verify; running it in CI too raises it",
2668
- L3: "L3 is the ceiling doctor can read: branch protection is what makes it blocking, and that lives in the API",
2669
- L4: "nothing above this"
2670
- },
2671
- weakestLinkNone: "WEAKEST LINK: nothing is claimed",
3077
+ uncollectedTests: "TESTS THE RECORD CARRIES AND THE RUNNER NEVER COLLECTS.",
3078
+ unreadableFiles: "FILES THE RECORD NAMES AND THIS PROBE COULD NOT OPEN: they are neither missing nor modified, and nothing is said about them.",
3079
+ executesNothing: "NOTHING HERE IS EXECUTED: doctor reads files and runs nothing from the repository it inspects, so it does not speak about whether the harness passes.",
3080
+ verdictHeld: (mechanism) => mechanism,
3081
+ verdictUnsupported: (mechanism, doesNotHold) => `EXPECTS ${mechanism} \u2014 no longer matching: ${doesNotHold.join(", ")}`,
3082
+ verdictUnevaluable: (mechanism, unevaluable) => `EXPECTS ${mechanism} \u2014 could not be read here, so nothing is said about it: ${unevaluable.join(", ")}`,
3083
+ verdictNothingNamed: (mechanism) => `EXPECTS ${mechanism} \u2014 no fact is named under it, so nothing was read`,
3084
+ enforcementNoModel: "THERE IS NO construct.model.json HERE: nothing was read, so nothing is known about what this repository claims \u2014 which is not a reading that nothing is enforced.",
3085
+ enforcementNoClaim: "construct.model.json NAMES NO CLAIM: it was read and it asserts nothing about this repository \u2014 which is not a reading that nothing is enforced.",
3086
+ youAreHereUnsupported: (claimId, stage, [first, ...rest]) => `YOU ARE HERE: ${claimId} \u2014 ${stage} unsupported: ${first} no longer matches${andMore(rest)}`,
3087
+ youAreHereUnevaluable: (claimId, stage, [first, ...rest]) => `YOU ARE HERE: ${claimId} \u2014 ${stage} unknown: ${first} could not be read${andMore(rest)}, so nothing is said about it`,
3088
+ youAreHereNothingNamed: (claimId, stage) => `YOU ARE HERE: ${claimId} \u2014 ${stage} unknown: no fact is named under it, so nothing was read`,
3089
+ youAreHereNone: "YOU ARE HERE: no claim stops before the end of its chain",
3090
+ youAreHereNoClaim: "YOU ARE HERE: construct.model.json carries no claim, so there is none to place",
3091
+ youAreHereNoModel: "YOU ARE HERE: nowhere to place you \u2014 there is no construct.model.json, so nothing is known about claims",
2672
3092
  wireHarness: "Existing configs were kept, so the harness is not wired in yet. /construct-discover does this first; by hand:",
2673
3093
  wireHarnessSteps: [
2674
3094
  "eslint: ignore scripts/construct/*.workflow.mjs (the ladder script uses top-level return)",
@@ -2714,7 +3134,8 @@ var LORE = {
2714
3134
  syncVersionGap: (from, to) => `ENGRAM CUT BY v${from} // REPLAYED BY v${to}`,
2715
3135
  syncNoManifest: "No construct.json here. Run `construct init` first.",
2716
3136
  recordCarriedOver: (carried, added) => `ENGRAM EXTENDED: ${carried} record${carried === 1 ? "" : "s"} carried over from the construct.json already here, ${added} added.`,
2717
- recordVarsChanged: (changed) => `ENGRAM REWRITTEN: this run changed ${changed.map((entry) => `${entry.name} (${entry.from} \u2192 ${entry.to})`).join(", ")} in the record; the recorded hashes were taken with the old value${changed.length === 1 ? "" : "s"}.`
3137
+ recordVarsChanged: (changed) => `ENGRAM REWRITTEN: this run changed ${changed.map((entry) => `${entry.name} (${entry.from} \u2192 ${entry.to})`).join(", ")} in the record; the recorded hashes were taken with the old value${changed.length === 1 ? "" : "s"}.`,
3138
+ recordFactsRetained: (facts, entries) => `ENGRAM HELD: ${facts.length} construct-authored fact${facts.length === 1 ? "" : "s"} this preset no longer makes ${facts.length === 1 ? "was" : "were"} kept, because ${entries.join(", ")} still ${entries.length === 1 ? "stands" : "stand"} on ${facts.length === 1 ? "it" : "them"}.`
2718
3139
  };
2719
3140
  var PLAIN_LORE = {
2720
3141
  subtitle: (version) => `mikoshi-construct v${version}`,
@@ -2751,15 +3172,21 @@ var PLAIN_LORE = {
2751
3172
  baselineGapUnknown: "What a sync would add or update cannot be established from this manifest: run `construct sync`.",
2752
3173
  enforcement: "Enforcement",
2753
3174
  typecheckCaveat: "Typecheck cannot carry this stack alone.",
2754
- weakestLink: (id, level) => `Weakest link: ${id} at ${level}`,
2755
- levelLift: {
2756
- L0: "wire it to a hook or a workflow step and it climbs",
2757
- L1: "only a reviewer stands behind it; a hook or a workflow step raises it",
2758
- L2: "a hook is bypassable with --no-verify; running it in CI too raises it",
2759
- L3: "L3 is the ceiling doctor can read: branch protection is what makes it blocking, and that lives in the API",
2760
- L4: "nothing above this"
2761
- },
2762
- weakestLinkNone: "Weakest link: nothing is claimed",
3175
+ uncollectedTests: "Tests the record carries and the runner does not collect.",
3176
+ unreadableFiles: "Files the record names that could not be read: they are neither missing nor modified, and nothing is said about them.",
3177
+ executesNothing: "doctor executes nothing from the repository it inspects, so it does not speak about whether the harness passes.",
3178
+ verdictHeld: (mechanism) => mechanism,
3179
+ verdictUnsupported: (mechanism, doesNotHold) => `expects ${mechanism} \u2014 no longer matching: ${doesNotHold.join(", ")}`,
3180
+ verdictUnevaluable: (mechanism, unevaluable) => `expects ${mechanism} \u2014 could not be read here, so nothing is said about it: ${unevaluable.join(", ")}`,
3181
+ verdictNothingNamed: (mechanism) => `expects ${mechanism} \u2014 no fact is named under it, so nothing was read`,
3182
+ enforcementNoModel: "There is no construct.model.json here: nothing was read, so nothing is known about what this repository claims \u2014 which is not a reading that nothing is enforced.",
3183
+ enforcementNoClaim: "construct.model.json names no claim: it was read and it asserts nothing about this repository \u2014 which is not a reading that nothing is enforced.",
3184
+ youAreHereUnsupported: (claimId, stage, [first, ...rest]) => `You are here: ${claimId} \u2014 ${stage} unsupported: ${first} no longer matches${andMore(rest)}`,
3185
+ youAreHereUnevaluable: (claimId, stage, [first, ...rest]) => `You are here: ${claimId} \u2014 ${stage} unknown: ${first} could not be read${andMore(rest)}, so nothing is said about it`,
3186
+ youAreHereNothingNamed: (claimId, stage) => `You are here: ${claimId} \u2014 ${stage} unknown: no fact is named under it, so nothing was read`,
3187
+ youAreHereNone: "You are here: no claim stops before the end of its chain",
3188
+ youAreHereNoClaim: "You are here: construct.model.json carries no claim, so there is none to place",
3189
+ youAreHereNoModel: "You are here: nowhere to place you \u2014 there is no construct.model.json, so nothing is known about claims",
2763
3190
  wireHarness: "Existing configs were kept, so the harness is not wired in yet. /construct-discover does this first; by hand:",
2764
3191
  wireHarnessSteps: [
2765
3192
  "eslint: ignore scripts/construct/*.workflow.mjs (the ladder script uses top-level return)",
@@ -2805,18 +3232,19 @@ var PLAIN_LORE = {
2805
3232
  syncVersionGap: (from, to) => `Materialized by construct ${from}, read by ${to}.`,
2806
3233
  syncNoManifest: "No construct.json here. Run `construct init` first.",
2807
3234
  recordCarriedOver: (carried, added) => `Carried over ${carried} record${carried === 1 ? "" : "s"} from the construct.json already here; added ${added}.`,
2808
- recordVarsChanged: (changed) => `This run changed ${changed.map((entry) => `${entry.name} (${entry.from} -> ${entry.to})`).join(", ")} in the record; the recorded hashes were taken with the old value${changed.length === 1 ? "" : "s"}.`
3235
+ recordVarsChanged: (changed) => `This run changed ${changed.map((entry) => `${entry.name} (${entry.from} -> ${entry.to})`).join(", ")} in the record; the recorded hashes were taken with the old value${changed.length === 1 ? "" : "s"}.`,
3236
+ recordFactsRetained: (facts, entries) => `Kept ${facts.length} construct-authored fact${facts.length === 1 ? "" : "s"} this preset no longer makes, because ${entries.join(", ")} still ${entries.length === 1 ? "stands" : "stand"} on ${facts.length === 1 ? "it" : "them"}.`
2809
3237
  };
2810
3238
 
2811
3239
  // src/ui/console.ts
2812
- var stdoutWriter = (text2) => {
2813
- process4.stdout.write(text2);
3240
+ var stdoutWriter = (text3) => {
3241
+ process4.stdout.write(text3);
2814
3242
  };
2815
3243
  function createUi(theme, write = stdoutWriter) {
2816
3244
  const plain = theme.name === "plain";
2817
3245
  const lore = plain ? PLAIN_LORE : LORE;
2818
- const out = (text2 = "") => {
2819
- write(`${text2}
3246
+ const out = (text3 = "") => {
3247
+ write(`${text3}
2820
3248
  `);
2821
3249
  };
2822
3250
  const icon = (glyph, fallback) => plain ? fallback : glyph;
@@ -2854,16 +3282,16 @@ function createUi(theme, write = stdoutWriter) {
2854
3282
  });
2855
3283
  },
2856
3284
  line: out,
2857
- ok(text2) {
2858
- out(`${icon("\u2705", "[ok]")} ${theme.ok(theme.bold(text2))}`);
3285
+ ok(text3) {
3286
+ out(`${icon("\u2705", "[ok]")} ${theme.ok(theme.bold(text3))}`);
2859
3287
  },
2860
- glitch(text2, details = []) {
2861
- out(`${icon("\u26A0", "[warn]")} ${theme.warn(`${lore.glitch}:`)} ${text2}`);
3288
+ glitch(text3, details = []) {
3289
+ out(`${icon("\u26A0", "[warn]")} ${theme.warn(`${lore.glitch}:`)} ${text3}`);
2862
3290
  for (const detail of details)
2863
3291
  out(` ${theme.dim(detail)}`);
2864
3292
  },
2865
- flatline(text2) {
2866
- out(`${icon("\u2620", "[error]")} ${theme.fail(`${lore.flatlined}:`)} ${text2}`);
3293
+ flatline(text3) {
3294
+ out(`${icon("\u2620", "[error]")} ${theme.fail(`${lore.flatlined}:`)} ${text3}`);
2867
3295
  }
2868
3296
  };
2869
3297
  }
@@ -2872,9 +3300,9 @@ function createUi(theme, write = stdoutWriter) {
2872
3300
  import process5 from "process";
2873
3301
  import pc from "picocolors";
2874
3302
  function rgb(r, g, b) {
2875
- return (text2) => `\x1B[38;2;${r};${g};${b}m${text2}\x1B[39m`;
3303
+ return (text3) => `\x1B[38;2;${r};${g};${b}m${text3}\x1B[39m`;
2876
3304
  }
2877
- var identity = (text2) => text2;
3305
+ var identity = (text3) => text3;
2878
3306
  function supportsColor() {
2879
3307
  if (process5.env.NO_COLOR != null && process5.env.NO_COLOR !== "")
2880
3308
  return false;
@@ -2907,7 +3335,7 @@ function arasaka() {
2907
3335
  dim: pc.dim,
2908
3336
  ok: tc ? rgb(0, 255, 159) : pc.green,
2909
3337
  warn: pc.yellow,
2910
- fail: (text2) => pc.bold(pc.red(text2)),
3338
+ fail: (text3) => pc.bold(pc.red(text3)),
2911
3339
  bold: pc.bold
2912
3340
  };
2913
3341
  }
@@ -3007,7 +3435,7 @@ var cost = defineCommand({
3007
3435
  json: { type: "boolean", description: "Machine-readable report", default: false }
3008
3436
  },
3009
3437
  run({ args }) {
3010
- const report = costReport(path22.resolve(args.dir));
3438
+ const report = costReport(path21.resolve(args.dir));
3011
3439
  if (args.json) {
3012
3440
  process6.stdout.write(`${JSON.stringify(costJson(report, args.last), null, 2)}
3013
3441
  `);