mikoshi-construct 0.4.0 → 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.
Files changed (2) hide show
  1. package/dist/cli.js +944 -522
  2. package/package.json +1 -1
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";
@@ -209,37 +209,37 @@ function toEntry(raw) {
209
209
  }
210
210
  function readLedger(root) {
211
211
  const file = path2.join(root, LEDGER_FILE);
212
- const reading = { entries: [], malformed: [] };
212
+ const reading2 = { entries: [], malformed: [] };
213
213
  if (!existsSync2(file))
214
- return reading;
215
- readFileSync2(file, "utf8").split("\n").forEach((text2, index) => {
214
+ return reading2;
215
+ readFileSync2(file, "utf8").split("\n").forEach((text3, index) => {
216
216
  const line = index + 1;
217
- if (text2.trim() === "")
217
+ if (text3.trim() === "")
218
218
  return;
219
219
  let raw;
220
220
  try {
221
- raw = JSON.parse(text2);
221
+ raw = JSON.parse(text3);
222
222
  } catch {
223
- reading.malformed.push({ line, reason: "not JSON" });
223
+ reading2.malformed.push({ line, reason: "not JSON" });
224
224
  return;
225
225
  }
226
226
  const entry = toEntry(raw);
227
227
  if (typeof entry === "string")
228
- reading.malformed.push({ line, reason: entry });
228
+ reading2.malformed.push({ line, reason: entry });
229
229
  else
230
- reading.entries.push(entry);
230
+ reading2.entries.push(entry);
231
231
  });
232
- return reading;
232
+ return reading2;
233
233
  }
234
- function summarizeLedger(reading) {
235
- const anyTokenUnknown = reading.entries.some((entry) => entry.tokens === "unknown");
236
- 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);
237
237
  return {
238
- runs: reading.entries.length,
239
- agents: reading.entries.reduce((total, entry) => total + entry.agents, 0),
240
- 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,
241
241
  tokens: anyTokenUnknown ? "unknown" : counted,
242
- malformed: reading.malformed
242
+ malformed: reading2.malformed
243
243
  };
244
244
  }
245
245
  function withoutTokenTotals(summary) {
@@ -541,8 +541,8 @@ function printCost(ui2, report, last) {
541
541
  // src/commands/cost/index.ts
542
542
  function costReport(cwd, options = {}) {
543
543
  const runtime = resolveRuntime(cwd, options.env ?? process2.env);
544
- const reading = readLedger(cwd);
545
- const ledger = summarizeLedger(reading);
544
+ const reading2 = readLedger(cwd);
545
+ const ledger = summarizeLedger(reading2);
546
546
  const reported = hasLedgerFindings(ledger);
547
547
  const source = runtime === "claude-code" ? new ClaudeCodeCostSource(options.projectsDir) : null;
548
548
  if (source == null || !source.readable())
@@ -553,19 +553,428 @@ function costReport(cwd, options = {}) {
553
553
  runtime,
554
554
  ...result,
555
555
  ...reported ? { ledger } : {},
556
- ...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) } : {}
557
557
  };
558
558
  }
559
559
 
560
- // src/version.ts
561
- import { readFileSync as readFileSync4 } from "fs";
560
+ // src/model/write.ts
561
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
562
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";
563
972
  import { fileURLToPath } from "url";
564
- var HERE = path5.dirname(fileURLToPath(import.meta.url));
973
+ var HERE = path6.dirname(fileURLToPath(import.meta.url));
565
974
  function readVersion() {
566
975
  for (const candidate of ["../package.json", "../../package.json"]) {
567
976
  try {
568
- const parsed = JSON.parse(readFileSync4(path5.resolve(HERE, candidate), "utf8"));
977
+ const parsed = JSON.parse(readFileSync5(path6.resolve(HERE, candidate), "utf8"));
569
978
  if (parsed.name === "mikoshi-construct" && parsed.version != null)
570
979
  return parsed.version;
571
980
  } catch {
@@ -576,219 +985,80 @@ function readVersion() {
576
985
  var VERSION = readVersion();
577
986
 
578
987
  // src/commands/doctor/baseline.ts
579
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
580
- import path6 from "path";
581
- function baselineVerdict(root, manifest) {
582
- const missingFiles = [];
583
- const modifiedFiles = [];
584
- for (const [file, hash] of Object.entries(manifest.files)) {
585
- const absolute = path6.join(root, file);
586
- if (!existsSync5(absolute))
587
- missingFiles.push(file);
588
- else if (sha256(readFileSync5(absolute, "utf8")) !== hash)
589
- modifiedFiles.push(file);
590
- }
591
- return { missingFiles, modifiedFiles };
592
- }
593
-
594
- // src/commands/doctor/enforcement.ts
595
- function harnessReach(evidence) {
596
- const command = `"${evidence.harness.command}"`;
597
- if (evidence.workflows.harnessWorkflow != null)
598
- return { level: "L3", evidence: `${evidence.workflows.harnessWorkflow} runs ${command}` };
599
- if (evidence.hooks.runsHarness && evidence.hooks.manager != null)
600
- return { level: "L2", evidence: `${evidence.hooks.manager} runs ${command}, and a local hook is bypassable with --no-verify` };
601
- return { level: "L0", evidence: `no workflow and no hook configuration runs ${command}` };
602
- }
603
- function reached(id, state, evidence, detail) {
604
- const reach = harnessReach(evidence);
605
- return { id, level: reach.level, state, evidence: `${detail}; ${reach.evidence}` };
606
- }
607
- function absent(id, detail) {
608
- return { id, level: "L0", state: "absent", evidence: detail };
609
- }
610
-
611
- // src/commands/doctor/checks/ci.ts
612
- var ID = "ci";
613
- 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";
614
- function ciCheck(evidence) {
615
- const workflows = evidence.workflows;
616
- const reach = harnessReach(evidence);
617
- if (workflows.harnessWorkflow != null)
618
- return { id: ID, level: reach.level, state: "present", evidence: `${reach.evidence}; ${SCOPE}` };
619
- 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}"`;
620
- return { id: ID, level: reach.level, state: "unknown", evidence: `${seen}; ${SCOPE}` };
621
- }
988
+ import { existsSync as existsSync7 } from "fs";
989
+ import path8 from "path";
622
990
 
623
- // src/commands/doctor/runner.ts
624
- 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";
625
993
  import path7 from "path";
626
- var RUNNER_CONFIG_FILES = [
627
- "vitest.config.ts",
628
- "vitest.config.mts",
629
- "vitest.config.js",
630
- "vitest.config.mjs",
631
- "vite.config.ts",
632
- "vite.config.mts",
633
- "vite.config.js",
634
- "vite.config.mjs"
635
- ];
636
- var STRING_LITERAL = /^(['"])(.*)\1$/;
637
- function literalEntries(body) {
638
- const trimmed = body.trim();
639
- if (trimmed === "")
640
- return [];
641
- const entries = trimmed.split(",").map((entry) => entry.trim()).filter((entry) => entry !== "");
642
- const values = entries.map((entry) => STRING_LITERAL.exec(entry)?.[2]);
643
- return values.every((value) => value != null) ? values : null;
644
- }
645
- function includeGlobs(source) {
646
- const found = [];
647
- let literal = false;
648
- for (const match of source.matchAll(/include\s*:\s*/g)) {
649
- const rest = source.slice(match.index + match[0].length);
650
- if (!rest.startsWith("["))
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))
651
1006
  return null;
652
- const close = rest.indexOf("]");
653
- if (close === -1)
654
- return null;
655
- const entries = literalEntries(rest.slice(1, close));
656
- if (entries == null)
1007
+ try {
1008
+ return readFileSync6(target, "utf8");
1009
+ } catch (error) {
1010
+ this.causes.set(file, cause(error));
657
1011
  return null;
658
- literal = true;
659
- found.push(...entries);
660
- }
661
- return literal ? found : null;
662
- }
663
- function escapeLiteral(character) {
664
- return /[.+^${}()|[\]\\]/.test(character) ? `\\${character}` : character;
665
- }
666
- function globToRegExp(glob) {
667
- let pattern = "";
668
- let braces = 0;
669
- for (let index = 0; index < glob.length; index += 1) {
670
- const character = glob[index];
671
- if (character === "*" && glob[index + 1] === "*" && glob[index + 2] === "/") {
672
- pattern += "(?:[^/]+/)*";
673
- index += 2;
674
- continue;
675
1012
  }
676
- if (character === "*" && glob[index + 1] === "*") {
677
- pattern += ".*";
678
- index += 1;
679
- continue;
680
- }
681
- if (character === "*") {
682
- pattern += "[^/]*";
683
- continue;
684
- }
685
- if (character === "?") {
686
- pattern += "[^/]";
687
- continue;
688
- }
689
- if (character === "{") {
690
- braces += 1;
691
- pattern += "(?:";
692
- continue;
693
- }
694
- if (character === "}" && braces > 0) {
695
- braces -= 1;
696
- pattern += ")";
697
- 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;
698
1023
  }
699
- if (character === "," && braces > 0) {
700
- pattern += "|";
701
- 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;
702
1034
  }
703
- pattern += escapeLiteral(character);
704
1035
  }
705
- return new RegExp(`^${pattern}$`);
706
- }
707
- function matchesAnyGlob(file, globs) {
708
- const normalized = file.replace(/^\.\//, "");
709
- return globs.some((glob) => globToRegExp(glob.replace(/^\.\//, "")).test(normalized));
710
- }
711
- function readRunnerFacts(root, harnessText) {
712
- const invokedByHarness = harnessText.includes("vitest");
713
- const file = RUNNER_CONFIG_FILES.find((candidate) => existsSync6(path7.join(root, candidate))) ?? null;
714
- if (file == null) {
715
- return {
716
- file: null,
717
- globs: null,
718
- note: `no runner config file (${RUNNER_CONFIG_FILES[0]} or a sibling) exists, so the include list cannot be read`,
719
- invokedByHarness
720
- };
1036
+ unreadable(file) {
1037
+ return this.causes.has(file);
721
1038
  }
722
- let source;
723
- try {
724
- source = readFileSync6(path7.join(root, file), "utf8");
725
- } catch {
726
- 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) ?? ""})`);
727
1041
  }
728
- const globs = includeGlobs(source);
729
- if (globs == null)
730
- 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 };
731
- return { file, globs, note: `${file} includes ${globs.map((glob) => `"${glob}"`).join(", ")}`, invokedByHarness };
732
- }
733
-
734
- // src/commands/doctor/checks/construct-tests.ts
735
- var ID2 = "construct-tests";
736
- function constructTestsCheck(evidence) {
737
- const recorded = evidence.recordedTests;
738
- if (recorded.length === 0)
739
- return absent(ID2, "construct.json records no test file; weakest link: there are no construct tests to run");
740
- const runner = evidence.runner;
741
- if (runner.globs == null)
742
- return reached(ID2, "unknown", evidence, `construct.json records ${recorded.length} test files, but ${runner.note}`);
743
- const orphan = recorded.find((file) => !matchesAnyGlob(file, runner.globs ?? []));
744
- if (orphan != null)
745
- 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`);
746
- if (!runner.invokedByHarness)
747
- 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`);
748
- 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`);
749
- }
750
-
751
- // src/commands/doctor/checks/hook.ts
752
- var ID3 = "hook";
753
- function hookCheck(evidence) {
754
- const hooks = evidence.hooks;
755
- if (hooks.manager != null) {
756
- const runs = hooks.runsHarness ? `runs "${evidence.harness.command}"` : `does not run "${evidence.harness.command}"`;
757
- return { id: ID3, level: "L2", state: "present", evidence: `${hooks.manager} installs a git hook and ${runs}; a local hook is bypassable with --no-verify` };
758
- }
759
- if (hooks.script != null)
760
- 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` };
761
- 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" };
762
- }
763
-
764
- // src/commands/doctor/checks/lint-policy.ts
765
- var ID4 = "lint-policy";
766
- function lintPolicyCheck(evidence) {
767
- const policy = evidence.policyTests[0];
768
- if (policy == null) {
769
- if (evidence.unreadable.length > 0)
770
- return reached(ID4, "unknown", evidence, `${evidence.unreadable[0]} cannot be read, so doctor cannot say whether a lint policy check exists`);
771
- 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");
772
- }
773
- const runner = evidence.runner;
774
- if (runner.globs == null)
775
- return reached(ID4, "unknown", evidence, `${policy} runs ESLint over the lint policy this repository declares, but ${runner.note}`);
776
- if (!matchesAnyGlob(policy, runner.globs))
777
- 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`);
778
- if (!runner.invokedByHarness)
779
- 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`);
780
- 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`);
781
- }
782
-
783
- // src/commands/doctor/checks/red-gate.ts
784
- var ID5 = "red-gate";
785
- function redGateCheck(evidence) {
786
- 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 };
787
1058
  }
788
1059
 
789
1060
  // src/commands/doctor/discovery.ts
790
- import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
791
- import path8 from "path";
1061
+ import path9 from "path";
792
1062
 
793
1063
  // src/detect/facts.ts
794
1064
  function factsTheRepositoryEstablishes(root) {
@@ -812,22 +1082,20 @@ function blockBody(document, marker) {
812
1082
  const body = document.slice(start + markerOpen(marker).length, stop).trim();
813
1083
  return body === "" || body === DISCOVERY_PLACEHOLDER ? null : body;
814
1084
  }
815
- function compositionBody(directory) {
816
- if (!existsSync7(directory))
817
- return null;
818
- const models = readdirSync3(directory).filter((file) => file.endsWith(".yaml")).sort();
819
- if (models.length === 0)
820
- return null;
821
- return models.map((model) => `${model}
822
- ${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");
823
1093
  }
824
- function markerBody(root, marker, file) {
825
- const location = path8.join(root, file);
1094
+ function markerBody(root, marker, file, readings = new FileReadings(root)) {
826
1095
  if (marker === "composition")
827
- return compositionBody(location);
828
- if (!existsSync7(location))
829
- return null;
830
- 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);
831
1099
  }
832
1100
  function markerFileFor(root, manifest, marker) {
833
1101
  const recordedFile = manifest.discovery.markers[marker].file;
@@ -838,18 +1106,18 @@ function markerFileFor(root, manifest, marker) {
838
1106
  return decided;
839
1107
  return factsTheRepositoryEstablishes(root).compositionDir ?? recordedFile;
840
1108
  }
841
- function missingDiscovery(root, manifest) {
842
- 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);
843
1111
  }
844
1112
 
845
- // src/commands/doctor/evidence.ts
846
- import { readFileSync as readFileSync11 } from "fs";
847
- 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
+ }
848
1117
 
849
1118
  // src/commands/doctor/harness.ts
850
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
851
- import path9 from "path";
852
- var REQUIRED_QUALITY_STEPS = ["lint", "typecheck", "test"];
1119
+ import { existsSync as existsSync8 } from "fs";
1120
+ import path10 from "path";
853
1121
  var SCRIPT_REFERENCE = /(?:^|&&|\|\||;)\s*(?:pnpm|npm|yarn|bun)\s+(?:run\s+)?([\w:.-]+)/g;
854
1122
  function harnessScriptName(command) {
855
1123
  return command.replace(/^(pnpm|npm|yarn|bun)\s+(run\s+)?/, "");
@@ -864,163 +1132,142 @@ function expandScript(scripts, name, seen) {
864
1132
  const referenced = [...body.matchAll(SCRIPT_REFERENCE)].map((match) => match[1]);
865
1133
  return [body, ...referenced.map((reference) => expandScript(scripts, reference, seen))].join(" && ");
866
1134
  }
867
- function readHarnessFacts(root, command) {
1135
+ var HARNESS_MANIFEST = "package.json";
1136
+ function readHarnessFacts(root, command, readings = new FileReadings(root)) {
868
1137
  const script = harnessScriptName(command);
869
- const manifestPath = path9.join(root, "package.json");
870
- const packageJson = existsSync8(manifestPath) ? JSON.parse(readFileSync8(manifestPath, "utf8")) : null;
1138
+ const packageJson = readings.readJson(HARNESS_MANIFEST);
871
1139
  const scripts = packageJson?.scripts ?? {};
872
1140
  const body = scripts[script] ?? null;
873
1141
  return {
874
1142
  command,
875
1143
  script,
876
- scripts,
877
1144
  body,
878
1145
  resolved: expandScript(scripts, script, /* @__PURE__ */ new Set()),
879
- commandForms: [command, `pnpm run ${script}`, `pnpm ${script}`, `npm run ${script}`, `yarn ${script}`],
880
1146
  packageJson
881
1147
  };
882
1148
  }
883
- function runsHarnessCommand(text2, forms) {
884
- return forms.some((form) => text2.includes(form));
885
- }
886
- function contractProblems(root, contracts, script, body) {
1149
+ function missingContractFiles(root, contracts) {
887
1150
  if (contracts == null)
888
1151
  return [];
889
- const problems = [contracts.path, contracts.types].filter((file) => !existsSync8(path9.join(root, file))).map((file) => `${file} is missing (construct.json \u2192 contracts)`);
890
- if (!body.includes("contracts:check"))
891
- problems.push(`"${script}" does not run contracts:check`);
892
- return problems;
1152
+ return [contracts.path, contracts.types].filter((file) => !existsSync8(path10.join(root, file))).map((file) => `${file} is missing (construct.json \u2192 contracts)`);
893
1153
  }
894
- function harnessProblems(root, manifest, facts) {
1154
+ function harnessProblems(root, manifest, facts, readings = new FileReadings(root)) {
895
1155
  if (facts.packageJson == null)
896
- return ["package.json is missing"];
897
- const body = facts.body;
898
- if (body == null)
1156
+ return readings.unreadable(HARNESS_MANIFEST) ? [] : [`${HARNESS_MANIFEST} is missing`];
1157
+ if (facts.body == null)
899
1158
  return [`package.json has no "${facts.script}" script (harness command is "${facts.command}")`];
900
- return [
901
- ...REQUIRED_QUALITY_STEPS.filter((step) => !body.includes(step)).map((step) => `"${facts.script}" does not run ${step}`),
902
- ...contractProblems(root, manifest.contracts, facts.script, body)
903
- ];
1159
+ return missingContractFiles(root, manifest.contracts);
904
1160
  }
905
1161
 
906
- // src/commands/doctor/hooks.ts
907
- import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync2 } from "fs";
908
- import path10 from "path";
909
- var LEFTHOOK_FILES = ["lefthook.yml", "lefthook.yaml", "lefthook.toml", "lefthook.json", ".lefthook.yml", ".lefthook.yaml"];
910
- 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"];
911
- var HUSKY_DIR = ".husky";
912
- var GIT_CONFIG = ".git/config";
913
- var HOOK_SCRIPTS = ["precommit", "pre-commit", "prepush", "pre-push"];
914
- function read(root, file) {
915
- try {
916
- return readFileSync9(path10.join(root, file), "utf8");
917
- } catch {
918
- 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 };
919
1169
  }
1170
+ return null;
920
1171
  }
921
- function huskyHooks(root) {
922
- const directory = path10.join(root, HUSKY_DIR);
923
- if (!existsSync9(directory) || !statSync2(directory).isDirectory())
924
- return [];
925
- return readdirSync4(directory).filter((entry) => !entry.startsWith("_") && !entry.startsWith(".")).sort().map((entry) => `${HUSKY_DIR}/${entry}`);
926
- }
927
- function managerFiles(root, packageJson) {
928
- const files = [...huskyHooks(root)];
929
- files.push(...[...LEFTHOOK_FILES, ...SIMPLE_GIT_HOOKS_FILES].filter((file) => existsSync9(path10.join(root, file))));
930
- if (packageJson != null && "simple-git-hooks" in packageJson)
931
- files.push("package.json (simple-git-hooks)");
932
- const gitConfig = read(root, GIT_CONFIG);
933
- if (gitConfig != null && gitConfig.includes("hooksPath"))
934
- files.push(`${GIT_CONFIG} (core.hooksPath)`);
935
- return files;
936
- }
937
- function readHookFacts(root, packageJson, scripts, commandForms) {
938
- const files = managerFiles(root, packageJson);
939
- const script = HOOK_SCRIPTS.find((name) => scripts[name] != null) ?? null;
940
- const sources = files.map((file) => {
941
- if (file.startsWith("package.json"))
942
- return JSON.stringify(packageJson?.["simple-git-hooks"] ?? "");
943
- return read(root, file.split(" ")[0]) ?? "";
944
- });
945
- const manager = files[0] ?? null;
946
- const scriptRunsHarness = script != null && runsHarnessCommand(scripts[script] ?? "", commandForms);
947
- const installed = sources.some((source) => runsHarnessCommand(source, commandForms) || scriptRunsHarness && script != null && source.includes(script));
948
- return { manager, script, runsHarness: manager != null && installed };
949
- }
950
-
951
- // src/commands/doctor/workflows.ts
952
- import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync10 } from "fs";
953
- import path11 from "path";
954
- var WORKFLOWS_DIR = ".github/workflows";
955
- var RUN_STEP = /(?:^|\s)run:[ \t]*(\S.*)$/;
956
- function runStepTexts(source) {
957
- const lines = source.split("\n");
958
- const steps = [];
959
- for (let index = 0; index < lines.length; index += 1) {
960
- const inline = RUN_STEP.exec(lines[index])?.[1];
961
- if (inline == null)
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)
962
1178
  continue;
963
- if (!inline.startsWith("|") && !inline.startsWith(">")) {
964
- steps.push(inline.trim());
1179
+ const stop = firstStop(claim.id, stages);
1180
+ if (stop === null)
965
1181
  continue;
1182
+ const depth = CHAIN_STAGES.indexOf(stop.stage);
1183
+ if (depth < selectedDepth) {
1184
+ selected = stop;
1185
+ selectedDepth = depth;
966
1186
  }
967
- const indent = lines[index].length - lines[index].trimStart().length;
968
- const block = [];
969
- for (let next = index + 1; next < lines.length; next += 1) {
970
- const line = lines[next];
971
- if (line.trim() !== "" && line.length - line.trimStart().length <= indent)
972
- break;
973
- block.push(line.trim());
974
- }
975
- steps.push(block.join("\n"));
976
- }
977
- return steps;
978
- }
979
- function readWorkflowFacts(root, commandForms) {
980
- const directory = path11.join(root, WORKFLOWS_DIR);
981
- if (!existsSync10(directory))
982
- return { directory: WORKFLOWS_DIR, files: [], harnessWorkflow: null, unreadable: [] };
983
- const files = readdirSync5(directory).filter((file) => file.endsWith(".yml") || file.endsWith(".yaml")).sort();
984
- const unreadable = [];
985
- let harnessWorkflow = null;
986
- for (const file of files) {
987
- let source;
988
- try {
989
- source = readFileSync10(path11.join(directory, file), "utf8");
990
- } catch {
991
- unreadable.push(`${WORKFLOWS_DIR}/${file}`);
992
- continue;
993
- }
994
- if (harnessWorkflow == null && runStepTexts(source).some((step) => runsHarnessCommand(step, commandForms)))
995
- harnessWorkflow = `${WORKFLOWS_DIR}/${file}`;
996
1187
  }
997
- return { directory: WORKFLOWS_DIR, files, harnessWorkflow, unreadable };
1188
+ return selected;
998
1189
  }
999
1190
 
1000
- // src/commands/doctor/evidence.ts
1001
- var TEST_FILE = /\.test\.[cm]?[jt]s$/;
1002
- var LINT_POLICY_MARKERS = ["calculateConfigForFile", "lintText", "lintFiles"];
1003
- function recordedTestFiles(manifest) {
1004
- return Object.keys(manifest.files).filter((file) => TEST_FILE.test(file)).sort();
1005
- }
1006
- function gatherEvidence(root, manifest) {
1007
- const harness = readHarnessFacts(root, manifest.harness.command);
1008
- const runner = readRunnerFacts(root, harness.resolved);
1009
- const workflows = readWorkflowFacts(root, harness.commandForms);
1010
- const hooks = readHookFacts(root, harness.packageJson, harness.scripts, harness.commandForms);
1011
- const recordedTests = recordedTestFiles(manifest);
1012
- const policyTests = [];
1013
- const unreadable = [...workflows.unreadable];
1014
- for (const file of recordedTests) {
1015
- try {
1016
- const source = readFileSync11(path12.join(root, file), "utf8");
1017
- if (LINT_POLICY_MARKERS.some((marker) => source.includes(marker)))
1018
- policyTests.push(file);
1019
- } catch {
1020
- unreadable.push(file);
1021
- }
1022
- }
1023
- 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) };
1024
1271
  }
1025
1272
 
1026
1273
  // src/commands/doctor/provenance.ts
@@ -1029,18 +1276,18 @@ function markerAuthorship(recorded, body) {
1029
1276
  return "unknown";
1030
1277
  return sha256(body) === recorded.sha ? "construct" : "owner";
1031
1278
  }
1032
- function discoveryProvenance(root, manifest) {
1279
+ function discoveryProvenance(root, manifest, readings = new FileReadings(root)) {
1033
1280
  return DISCOVERY_MARKERS.map((marker) => {
1034
1281
  const recorded = manifest.discovery.markers[marker];
1035
1282
  return {
1036
1283
  marker,
1037
1284
  file: recorded.file,
1038
- authorship: markerAuthorship(recorded, markerBody(root, marker, markerFileFor(root, manifest, marker)))
1285
+ authorship: markerAuthorship(recorded, markerBody(root, marker, markerFileFor(root, manifest, marker), readings))
1039
1286
  };
1040
1287
  });
1041
1288
  }
1042
- function constructAuthored(readings) {
1043
- return readings.filter((reading) => reading.authorship === "construct");
1289
+ function constructAuthored(markers) {
1290
+ return markers.filter((reading2) => reading2.authorship === "construct");
1044
1291
  }
1045
1292
 
1046
1293
  // src/commands/doctor/typecheck.ts
@@ -1050,33 +1297,123 @@ var CAVEATS = {
1050
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"
1051
1298
  }
1052
1299
  };
1053
- function typecheckWarnings(preset, evidence) {
1300
+ function typecheckWarnings(preset, harness) {
1054
1301
  const caveat = CAVEATS[preset];
1055
- if (caveat == null || caveat.checkers.some((checker) => evidence.harness.resolved.includes(checker)))
1302
+ if (caveat == null || caveat.checkers.some((checker) => harness.resolved.includes(checker)))
1056
1303
  return [];
1057
1304
  return [caveat.text];
1058
1305
  }
1059
1306
 
1060
- // src/commands/doctor/verdict.ts
1061
- var CHECK_IDS = ["lint-policy", "construct-tests", "ci", "hook", "red-gate"];
1062
- var LEVELS = ["L0", "L1", "L2", "L3", "L4"];
1063
- function weakestLink(checks) {
1064
- const claimed = checks.filter((check) => check.state === "present");
1065
- if (claimed.length === 0)
1066
- return null;
1067
- const ordered = [...claimed].sort((left, right) => CHECK_IDS.indexOf(left.id) - CHECK_IDS.indexOf(right.id));
1068
- const weakest = ordered.reduce((current, check) => LEVELS.indexOf(check.level) < LEVELS.indexOf(current.level) ? check : current);
1069
- 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();
1070
1407
  }
1071
1408
 
1072
1409
  // src/sync/replay.ts
1073
- import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
1410
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
1074
1411
  import { tmpdir } from "os";
1075
- import path15 from "path";
1412
+ import path14 from "path";
1076
1413
 
1077
1414
  // src/materialize/plan.ts
1078
- import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
1079
- import path14 from "path";
1415
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
1416
+ import path13 from "path";
1080
1417
 
1081
1418
  // src/materialize/rules.ts
1082
1419
  var CLAUDE_RULES_DIR = ".claude/rules/";
@@ -1255,43 +1592,43 @@ ${end}
1255
1592
  }
1256
1593
 
1257
1594
  // src/materialize/templates.ts
1258
- import { existsSync as existsSync11, readdirSync as readdirSync6, statSync as statSync3 } from "fs";
1259
- import path13 from "path";
1595
+ import { existsSync as existsSync10, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
1596
+ import path12 from "path";
1260
1597
  import { fileURLToPath as fileURLToPath2 } from "url";
1261
- var HERE2 = path13.dirname(fileURLToPath2(import.meta.url));
1598
+ var HERE2 = path12.dirname(fileURLToPath2(import.meta.url));
1262
1599
  function templatesRoot() {
1263
- const candidates = [path13.resolve(HERE2, "../templates"), path13.resolve(HERE2, "../../templates")];
1264
- 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));
1265
1602
  if (found == null)
1266
1603
  throw new Error(`templates directory not found next to ${HERE2}`);
1267
1604
  return found;
1268
1605
  }
1269
1606
  var EXISTING_SUFFIX = ".existing.eta";
1270
1607
  function toTargetPath(relative) {
1271
- 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);
1272
1609
  const joined = segments.join("/");
1273
1610
  if (joined.endsWith(EXISTING_SUFFIX))
1274
1611
  return { target: joined.slice(0, -EXISTING_SUFFIX.length), rendered: true, variant: "existing" };
1275
1612
  return joined.endsWith(".eta") ? { target: joined.slice(0, -".eta".length), rendered: true, variant: "default" } : { target: joined, rendered: false, variant: "default" };
1276
1613
  }
1277
1614
  function walk(root, current, files) {
1278
- for (const entry of readdirSync6(current).sort()) {
1279
- const absolute = path13.join(current, entry);
1280
- if (statSync3(absolute).isDirectory())
1615
+ for (const entry of readdirSync4(current).sort()) {
1616
+ const absolute = path12.join(current, entry);
1617
+ if (statSync2(absolute).isDirectory())
1281
1618
  walk(root, absolute, files);
1282
1619
  else if (entry !== ".DS_Store")
1283
- files.push(path13.relative(root, absolute));
1620
+ files.push(path12.relative(root, absolute));
1284
1621
  }
1285
1622
  }
1286
1623
  function listTemplateFiles(group) {
1287
- const root = path13.join(templatesRoot(), group);
1288
- if (!existsSync11(root))
1624
+ const root = path12.join(templatesRoot(), group);
1625
+ if (!existsSync10(root))
1289
1626
  throw new Error(`template group "${group}" does not exist`);
1290
1627
  const files = [];
1291
1628
  walk(root, root, files);
1292
1629
  return files.map((relative) => {
1293
1630
  const { target, rendered, variant } = toTargetPath(relative);
1294
- return { group, source: path13.join(root, relative), target, rendered, variant };
1631
+ return { group, source: path12.join(root, relative), target, rendered, variant };
1295
1632
  });
1296
1633
  }
1297
1634
  var BLOCK = /^[ \t]*\{\{#(if|unless) (\w+)\}\}[ \t]*\n([\s\S]*?)^[ \t]*\{\{\/\1\}\}[ \t]*\n/gm;
@@ -1318,7 +1655,7 @@ function mountTarget(mount, target) {
1318
1655
  return mount.into == null || mount.into === "." ? target : `${mount.into.replace(/\/$/, "")}/${target}`;
1319
1656
  }
1320
1657
  function readTemplate(source, rendered, vars) {
1321
- const raw = readFileSync12(source, "utf8");
1658
+ const raw = readFileSync8(source, "utf8");
1322
1659
  return rendered ? render(raw, vars) : raw;
1323
1660
  }
1324
1661
  var SORTED_SECTIONS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
@@ -1376,13 +1713,13 @@ function layerJson(earlier, later) {
1376
1713
  var NOT_ADDED_TO_EXISTING_MANIFEST = ["version"];
1377
1714
  function planOne(root, target, content, conflicts, existingVariant) {
1378
1715
  const strategy = strategyFor(target);
1379
- const absolute = path14.join(root, target);
1380
- const exists = existsSync12(absolute);
1716
+ const absolute = path13.join(root, target);
1717
+ const exists = existsSync11(absolute);
1381
1718
  if (!exists) {
1382
1719
  return strategy === "append-block" ? { target, strategy, action: "create", content: appendBlock("", content, target), variant: "default" } : { target, strategy, action: "create", content };
1383
1720
  }
1384
1721
  if (strategy === "merge-json") {
1385
- const existing = JSON.parse(readFileSync12(absolute, "utf8"));
1722
+ const existing = JSON.parse(readFileSync8(absolute, "utf8"));
1386
1723
  const incoming = JSON.parse(content);
1387
1724
  for (const key of NOT_ADDED_TO_EXISTING_MANIFEST)
1388
1725
  delete incoming[key];
@@ -1393,7 +1730,7 @@ function planOne(root, target, content, conflicts, existingVariant) {
1393
1730
  ` };
1394
1731
  }
1395
1732
  if (strategy === "append-block") {
1396
- const existing = readFileSync12(absolute, "utf8");
1733
+ const existing = readFileSync8(absolute, "utf8");
1397
1734
  return {
1398
1735
  target,
1399
1736
  strategy,
@@ -1552,6 +1889,9 @@ function isPresetId(value) {
1552
1889
  function getPreset(id) {
1553
1890
  return PRESETS[id];
1554
1891
  }
1892
+ function sampleGroups(preset) {
1893
+ return preset.groups.map((group) => typeof group === "string" ? group : group.group).filter((group) => group.endsWith("/sample"));
1894
+ }
1555
1895
  function aiGroups(target) {
1556
1896
  return target === "both" ? ["ai/shared", "ai/claude", "ai/cursor"] : ["ai/shared", `ai/${target}`];
1557
1897
  }
@@ -1734,7 +2074,7 @@ function establishVariant(input) {
1734
2074
  }
1735
2075
 
1736
2076
  // src/sync/replay.ts
1737
- 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");
1738
2078
  function replayedGroups(manifest) {
1739
2079
  const preset = getPreset(manifest.preset);
1740
2080
  return [...preset.groups, ...aiGroups(manifest.ai), ...reviewGroups(manifest.review?.provider ?? "none")];
@@ -1807,9 +2147,9 @@ function producedInTheVariantThatWroteIt(templates, variants) {
1807
2147
  function presentInTree(root, targets) {
1808
2148
  const present = {};
1809
2149
  for (const target of new Set(targets)) {
1810
- const absolute = path15.join(root, target);
1811
- if (existsSync13(absolute))
1812
- present[target] = readFileSync13(absolute, "utf8");
2150
+ const absolute = path14.join(root, target);
2151
+ if (existsSync12(absolute))
2152
+ present[target] = readFileSync9(absolute, "utf8");
1813
2153
  }
1814
2154
  return present;
1815
2155
  }
@@ -1884,14 +2224,39 @@ function versionGap(root, manifest, version) {
1884
2224
  }
1885
2225
 
1886
2226
  // src/commands/doctor/report.ts
1887
- function checkLine(ui2, check) {
1888
- 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
+ }
1889
2249
  }
1890
- function printChecks(ui2, checks) {
2250
+ function printChecks(ui2, checks, placement) {
2251
+ const width = Math.max(16, ...checks.map((check) => check.id.length));
1891
2252
  ui2.line();
1892
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}`));
1893
2257
  for (const check of checks)
1894
- ui2.line(checkLine(ui2, check));
2258
+ ui2.line(checkLine(ui2, check, width));
2259
+ ui2.line(ui2.theme.dim(` ${ui2.lore.executesNothing}`));
1895
2260
  }
1896
2261
  function printProvenance(ui2, provenance) {
1897
2262
  const authored = constructAuthored(provenance);
@@ -1899,8 +2264,8 @@ function printProvenance(ui2, provenance) {
1899
2264
  return;
1900
2265
  ui2.line();
1901
2266
  ui2.line(ui2.theme.accent(ui2.lore.provenance));
1902
- for (const reading of authored)
1903
- 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)}`);
1904
2269
  ui2.line(ui2.theme.dim(` ${ui2.lore.stillConstructAuthored(authored.length)}`));
1905
2270
  }
1906
2271
  function gapReading(ui2, gap) {
@@ -1912,14 +2277,29 @@ function printVersionGap(ui2, gap) {
1912
2277
  ui2.line(ui2.theme.dim(` ${ui2.lore.syncVersionGap(gap.materializedBy, gap.readBy)}`));
1913
2278
  ui2.line(ui2.theme.dim(` ${gapReading(ui2, gap)}`));
1914
2279
  }
1915
- function printWeakestLink(ui2, weakest) {
1916
- ui2.line();
1917
- if (weakest == null) {
1918
- ui2.line(ui2.theme.bold(ui2.lore.weakestLinkNone));
1919
- 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;
1920
2298
  }
1921
- const lift = ui2.lore.levelLift[weakest.level];
1922
- 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)));
1923
2303
  }
1924
2304
  function printDoctor(ui2, result) {
1925
2305
  if (result == null) {
@@ -1928,6 +2308,10 @@ function printDoctor(ui2, result) {
1928
2308
  }
1929
2309
  if (result.harnessProblems.length > 0)
1930
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);
1931
2315
  if (result.missingFiles.length > 0)
1932
2316
  ui2.glitch("Baseline files are missing.", result.missingFiles);
1933
2317
  if (result.missingDiscovery.length > 0)
@@ -1940,8 +2324,8 @@ function printDoctor(ui2, result) {
1940
2324
  if (result.ok)
1941
2325
  ui2.ok(ui2.lore.stable);
1942
2326
  printProvenance(ui2, result.provenance);
1943
- printChecks(ui2, result.checks);
1944
- printWeakestLink(ui2, result.weakestLink);
2327
+ printChecks(ui2, result.checks, result.youAreHere);
2328
+ printYouAreHere(ui2, result.youAreHere);
1945
2329
  return result.ok ? 0 : 1;
1946
2330
  }
1947
2331
 
@@ -1950,54 +2334,66 @@ function runDoctor(root, version = VERSION) {
1950
2334
  const manifest = readManifest(root);
1951
2335
  if (manifest == null)
1952
2336
  return null;
1953
- const evidence = gatherEvidence(root, manifest);
1954
- const baseline = baselineVerdict(root, manifest);
1955
- const problems = harnessProblems(root, manifest, evidence.harness);
1956
- const checks = [
1957
- lintPolicyCheck(evidence),
1958
- constructTestsCheck(evidence),
1959
- ciCheck(evidence),
1960
- hookCheck(evidence),
1961
- redGateCheck(evidence)
1962
- ];
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
+ };
1963
2357
  return {
1964
- ok: baseline.missingFiles.length === 0 && problems.length === 0,
2358
+ ok: isIntact(intact),
1965
2359
  missingFiles: baseline.missingFiles,
1966
2360
  modifiedFiles: baseline.modifiedFiles,
1967
- missingDiscovery: missingDiscovery(root, manifest),
1968
- provenance: discoveryProvenance(root, manifest),
2361
+ unreadableFiles,
2362
+ missingDiscovery: markers,
2363
+ provenance,
1969
2364
  harnessProblems: problems,
1970
- warnings: typecheckWarnings(manifest.preset, evidence),
1971
- checks,
1972
- weakestLink: weakestLink(checks),
2365
+ uncollectedTests: uncollected,
2366
+ warnings: typecheckWarnings(manifest.preset, harness),
2367
+ checks: knowledge.checks,
2368
+ youAreHere: knowledge.youAreHere,
1973
2369
  versionGap: versionGap(root, manifest, version)
1974
2370
  };
1975
2371
  }
1976
2372
 
1977
2373
  // src/commands/init.ts
1978
2374
  import { mkdirSync as mkdirSync2 } from "fs";
1979
- import path21 from "path";
2375
+ import path20 from "path";
1980
2376
 
1981
2377
  // src/detect/index.ts
1982
- import { existsSync as existsSync17 } from "fs";
1983
- import path19 from "path";
2378
+ import { existsSync as existsSync16 } from "fs";
2379
+ import path18 from "path";
1984
2380
  import process3 from "process";
1985
2381
 
1986
2382
  // src/detect/layout.ts
1987
- import { existsSync as existsSync15, readdirSync as readdirSync7, readFileSync as readFileSync15, statSync as statSync4 } from "fs";
1988
- 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";
1989
2385
 
1990
2386
  // src/detect/workspaces.ts
1991
- import { existsSync as existsSync14, readFileSync as readFileSync14 } from "fs";
1992
- import path16 from "path";
2387
+ import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
2388
+ import path15 from "path";
1993
2389
  var TOP_LEVEL_PACKAGES_KEY = /^packages:(.*)$/m;
1994
2390
  var EMPTY_FLOW_SEQUENCE = /^\[\s*\]$/;
1995
2391
  var BLOCK_SEQUENCE_ENTRY = /^[ \t]+-[ \t]*\S/;
1996
2392
  function readIfPresent(file) {
1997
- if (!existsSync14(file))
2393
+ if (!existsSync13(file))
1998
2394
  return null;
1999
2395
  try {
2000
- return readFileSync14(file, "utf8");
2396
+ return readFileSync10(file, "utf8");
2001
2397
  } catch {
2002
2398
  return null;
2003
2399
  }
@@ -2011,7 +2407,7 @@ function startsBlockSequence(rest) {
2011
2407
  return false;
2012
2408
  }
2013
2409
  function declaresPnpmPackages(dir) {
2014
- const content = readIfPresent(path16.join(dir, "pnpm-workspace.yaml"));
2410
+ const content = readIfPresent(path15.join(dir, "pnpm-workspace.yaml"));
2015
2411
  if (content == null)
2016
2412
  return false;
2017
2413
  const match = TOP_LEVEL_PACKAGES_KEY.exec(content);
@@ -2025,7 +2421,7 @@ function declaresPnpmPackages(dir) {
2025
2421
  return startsBlockSequence(content.slice(match.index + match[0].length));
2026
2422
  }
2027
2423
  function declaresNpmWorkspaces(dir) {
2028
- const content = readIfPresent(path16.join(dir, "package.json"));
2424
+ const content = readIfPresent(path15.join(dir, "package.json"));
2029
2425
  if (content == null)
2030
2426
  return false;
2031
2427
  let workspaces;
@@ -2043,9 +2439,9 @@ function declaresNpmWorkspaces(dir) {
2043
2439
  // src/detect/layout.ts
2044
2440
  var IGNORED_ENTRIES = /* @__PURE__ */ new Set([".git", ".DS_Store", ".gitignore", ".gitattributes", "LICENSE", "README.md", ".idea", ".vscode"]);
2045
2441
  function isEmptyDir(dir) {
2046
- if (!existsSync15(dir))
2442
+ if (!existsSync14(dir))
2047
2443
  return true;
2048
- return readdirSync7(dir).every((entry) => IGNORED_ENTRIES.has(entry));
2444
+ return readdirSync5(dir).every((entry) => IGNORED_ENTRIES.has(entry));
2049
2445
  }
2050
2446
  function detectMonorepoTools(dir) {
2051
2447
  const tools = [];
@@ -2053,40 +2449,40 @@ function detectMonorepoTools(dir) {
2053
2449
  tools.push("pnpm-workspace");
2054
2450
  if (declaresNpmWorkspaces(dir))
2055
2451
  tools.push("npm-workspaces");
2056
- if (existsSync15(path17.join(dir, "turbo.json")))
2452
+ if (existsSync14(path16.join(dir, "turbo.json")))
2057
2453
  tools.push("turbo");
2058
- if (existsSync15(path17.join(dir, "nx.json")))
2454
+ if (existsSync14(path16.join(dir, "nx.json")))
2059
2455
  tools.push("nx");
2060
2456
  return tools;
2061
2457
  }
2062
2458
  function detectWorkspaceDirs(dir) {
2063
- 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());
2064
2460
  }
2065
2461
  function packageName(dir) {
2066
2462
  try {
2067
- const parsed = JSON.parse(readFileSync15(path17.join(dir, "package.json"), "utf8"));
2463
+ const parsed = JSON.parse(readFileSync11(path16.join(dir, "package.json"), "utf8"));
2068
2464
  return typeof parsed.name === "string" && parsed.name !== "" ? parsed.name : null;
2069
2465
  } catch {
2070
2466
  return null;
2071
2467
  }
2072
2468
  }
2073
2469
  function detectWorkspacePackages(root, workspaceDirs) {
2074
- 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 })));
2075
2471
  }
2076
2472
  function detectLayout(dir, monorepoTools, workspaceDirs, hasSrc) {
2077
2473
  if (isEmptyDir(dir))
2078
2474
  return "empty";
2079
2475
  if (monorepoTools.length > 0 || workspaceDirs.length > 0)
2080
2476
  return "monorepo";
2081
- if (hasSrc || existsSync15(path17.join(dir, "package.json")))
2477
+ if (hasSrc || existsSync14(path16.join(dir, "package.json")))
2082
2478
  return "single";
2083
2479
  return "unknown";
2084
2480
  }
2085
2481
 
2086
2482
  // src/detect/package-manager.ts
2087
2483
  import { execFileSync } from "child_process";
2088
- import { existsSync as existsSync16, readFileSync as readFileSync16 } from "fs";
2089
- import path18 from "path";
2484
+ import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
2485
+ import path17 from "path";
2090
2486
  var LOCKFILES = [
2091
2487
  ["pnpm-lock.yaml", "pnpm"],
2092
2488
  ["bun.lockb", "bun"],
@@ -2095,11 +2491,11 @@ var LOCKFILES = [
2095
2491
  ["package-lock.json", "npm"]
2096
2492
  ];
2097
2493
  function fromPackageManagerField(dir) {
2098
- const manifest = path18.join(dir, "package.json");
2099
- if (!existsSync16(manifest))
2494
+ const manifest = path17.join(dir, "package.json");
2495
+ if (!existsSync15(manifest))
2100
2496
  return null;
2101
2497
  try {
2102
- const parsed = JSON.parse(readFileSync16(manifest, "utf8"));
2498
+ const parsed = JSON.parse(readFileSync12(manifest, "utf8"));
2103
2499
  const name = parsed.packageManager?.split("@")[0];
2104
2500
  return name === "pnpm" || name === "npm" || name === "yarn" || name === "bun" ? name : null;
2105
2501
  } catch {
@@ -2111,10 +2507,10 @@ function detectPackageManager(dir) {
2111
2507
  if (declared != null)
2112
2508
  return declared;
2113
2509
  for (const [lockfile, manager] of LOCKFILES) {
2114
- if (existsSync16(path18.join(dir, lockfile)))
2510
+ if (existsSync15(path17.join(dir, lockfile)))
2115
2511
  return manager;
2116
2512
  }
2117
- return existsSync16(path18.join(dir, "package.json")) ? "npm" : "none";
2513
+ return existsSync15(path17.join(dir, "package.json")) ? "npm" : "none";
2118
2514
  }
2119
2515
  var pnpmVersionCache;
2120
2516
  function detectPnpmVersion() {
@@ -2130,10 +2526,10 @@ function detectPnpmVersion() {
2130
2526
 
2131
2527
  // src/detect/index.ts
2132
2528
  function detect(dir) {
2133
- const root = path19.resolve(dir);
2529
+ const root = path18.resolve(dir);
2134
2530
  const monorepoTools = detectMonorepoTools(root);
2135
2531
  const workspaceDirs = detectWorkspaceDirs(root);
2136
- const hasSrc = existsSync17(path19.join(root, "src"));
2532
+ const hasSrc = existsSync16(path18.join(root, "src"));
2137
2533
  return {
2138
2534
  dir: root,
2139
2535
  packageManager: detectPackageManager(root),
@@ -2149,23 +2545,23 @@ function detect(dir) {
2149
2545
  }
2150
2546
 
2151
2547
  // src/materialize/apply.ts
2152
- import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
2153
- import path20 from "path";
2548
+ import { mkdirSync, writeFileSync as writeFileSync3 } from "fs";
2549
+ import path19 from "path";
2154
2550
  function applyPlan(root, ops) {
2155
2551
  const written = [];
2156
2552
  for (const op of ops) {
2157
2553
  if (op.action === "skip")
2158
2554
  continue;
2159
- const absolute = path20.join(root, op.target);
2160
- mkdirSync(path20.dirname(absolute), { recursive: true });
2161
- writeFileSync2(absolute, op.content);
2555
+ const absolute = path19.join(root, op.target);
2556
+ mkdirSync(path19.dirname(absolute), { recursive: true });
2557
+ writeFileSync3(absolute, op.content);
2162
2558
  written.push(op);
2163
2559
  }
2164
2560
  return written;
2165
2561
  }
2166
2562
 
2167
2563
  // src/ui/prompts.ts
2168
- import { cancel, confirm, isCancel, multiselect, select, text } from "@clack/prompts";
2564
+ import { cancel, confirm, isCancel, multiselect, select, text as text2 } from "@clack/prompts";
2169
2565
  var PROJECT_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
2170
2566
  function isValidProjectName(value) {
2171
2567
  return PROJECT_NAME_PATTERN.test(value);
@@ -2216,7 +2612,7 @@ function createClackPrompter(lore, streams = {}) {
2216
2612
  return selected == null ? void 0 : toAiTarget(selected);
2217
2613
  },
2218
2614
  async projectName(initial) {
2219
- const answer = await text({
2615
+ const answer = await text2({
2220
2616
  ...streams,
2221
2617
  message: lore.askName,
2222
2618
  initialValue: initial,
@@ -2331,7 +2727,7 @@ async function askChoices(ui2, options, report, root, prompter) {
2331
2727
  return { presetId, ai, projectName, review };
2332
2728
  }
2333
2729
  async function runInit(ui2, options, prompter) {
2334
- const root = path21.resolve(options.dir);
2730
+ const root = path20.resolve(options.dir);
2335
2731
  mkdirSync2(root, { recursive: true });
2336
2732
  if (!options.yes && prompter == null) {
2337
2733
  ui2.glitch(ui2.lore.needsTerminal);
@@ -2407,10 +2803,19 @@ async function runInit(ui2, options, prompter) {
2407
2803
  }
2408
2804
  if (interactive != null && await interactive.confirm(ui2.lore.confirm) !== true)
2409
2805
  return aborted(skipped, plan.conflicts);
2806
+ const existingModel = readModel(root);
2410
2807
  const written = applyPlan(root, plan.ops);
2411
2808
  const previous = readManifest(root);
2412
2809
  const manifest = buildManifest({ version: VERSION, preset: presetId, ai, review, vars, written, contracts: preset.contracts, previous });
2413
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
+ }
2414
2819
  if (previous != null) {
2415
2820
  const carriedOver = Object.keys(previous.files).filter((target) => !written.some((op) => op.target === target)).length;
2416
2821
  const added = written.filter((op) => previous.files[op.target] == null).length;
@@ -2501,8 +2906,8 @@ function printPaths(ui2, report) {
2501
2906
  }
2502
2907
  }
2503
2908
  function printMergedNote(ui2, report) {
2504
- const listed = report.classifications.filter((entry) => LISTED_CLASSES.includes(entry.class));
2505
- 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"))
2506
2911
  return;
2507
2912
  ui2.line();
2508
2913
  ui2.line(ui2.theme.dim(ui2.lore.syncMergedNotWritten));
@@ -2631,6 +3036,9 @@ var BANNER = String.raw`
2631
3036
  ██║╚██╔╝██║██║██╔═██╗ ██║ ██║╚════██║██╔══██║██║
2632
3037
  ██║ ╚═╝ ██║██║██║ ██╗╚██████╔╝███████║██║ ██║██║
2633
3038
  ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝`;
3039
+ function andMore(rest) {
3040
+ return rest.length === 0 ? "" : `, and ${rest.length} more ${rest.length === 1 ? "fact" : "facts"}`;
3041
+ }
2634
3042
  var LORE = {
2635
3043
  subtitle: (version) => `--- CONSTRUCT ENGINE v${version} // ARASAKA SUB-NET ---`,
2636
3044
  johnnyWakeUp: "Wake up, Netrunner. We have a repository to build.",
@@ -2666,15 +3074,21 @@ var LORE = {
2666
3074
  baselineGapUnknown: "What a sync would add or update cannot be established from this manifest: run `construct sync`.",
2667
3075
  enforcement: "ENFORCEMENT TRACE",
2668
3076
  typecheckCaveat: "Typecheck cannot carry this stack alone.",
2669
- weakestLink: (id, level) => `WEAKEST LINK: ${id} at ${level}`,
2670
- levelLift: {
2671
- L0: "wire it to a hook or a workflow step and it climbs",
2672
- L1: "only a reviewer stands behind it; a hook or a workflow step raises it",
2673
- L2: "a hook is bypassable with --no-verify; running it in CI too raises it",
2674
- L3: "L3 is the ceiling doctor can read: branch protection is what makes it blocking, and that lives in the API",
2675
- L4: "nothing above this"
2676
- },
2677
- 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",
2678
3092
  wireHarness: "Existing configs were kept, so the harness is not wired in yet. /construct-discover does this first; by hand:",
2679
3093
  wireHarnessSteps: [
2680
3094
  "eslint: ignore scripts/construct/*.workflow.mjs (the ladder script uses top-level return)",
@@ -2720,7 +3134,8 @@ var LORE = {
2720
3134
  syncVersionGap: (from, to) => `ENGRAM CUT BY v${from} // REPLAYED BY v${to}`,
2721
3135
  syncNoManifest: "No construct.json here. Run `construct init` first.",
2722
3136
  recordCarriedOver: (carried, added) => `ENGRAM EXTENDED: ${carried} record${carried === 1 ? "" : "s"} carried over from the construct.json already here, ${added} added.`,
2723
- 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"}.`
2724
3139
  };
2725
3140
  var PLAIN_LORE = {
2726
3141
  subtitle: (version) => `mikoshi-construct v${version}`,
@@ -2757,15 +3172,21 @@ var PLAIN_LORE = {
2757
3172
  baselineGapUnknown: "What a sync would add or update cannot be established from this manifest: run `construct sync`.",
2758
3173
  enforcement: "Enforcement",
2759
3174
  typecheckCaveat: "Typecheck cannot carry this stack alone.",
2760
- weakestLink: (id, level) => `Weakest link: ${id} at ${level}`,
2761
- levelLift: {
2762
- L0: "wire it to a hook or a workflow step and it climbs",
2763
- L1: "only a reviewer stands behind it; a hook or a workflow step raises it",
2764
- L2: "a hook is bypassable with --no-verify; running it in CI too raises it",
2765
- L3: "L3 is the ceiling doctor can read: branch protection is what makes it blocking, and that lives in the API",
2766
- L4: "nothing above this"
2767
- },
2768
- 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",
2769
3190
  wireHarness: "Existing configs were kept, so the harness is not wired in yet. /construct-discover does this first; by hand:",
2770
3191
  wireHarnessSteps: [
2771
3192
  "eslint: ignore scripts/construct/*.workflow.mjs (the ladder script uses top-level return)",
@@ -2811,18 +3232,19 @@ var PLAIN_LORE = {
2811
3232
  syncVersionGap: (from, to) => `Materialized by construct ${from}, read by ${to}.`,
2812
3233
  syncNoManifest: "No construct.json here. Run `construct init` first.",
2813
3234
  recordCarriedOver: (carried, added) => `Carried over ${carried} record${carried === 1 ? "" : "s"} from the construct.json already here; added ${added}.`,
2814
- 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"}.`
2815
3237
  };
2816
3238
 
2817
3239
  // src/ui/console.ts
2818
- var stdoutWriter = (text2) => {
2819
- process4.stdout.write(text2);
3240
+ var stdoutWriter = (text3) => {
3241
+ process4.stdout.write(text3);
2820
3242
  };
2821
3243
  function createUi(theme, write = stdoutWriter) {
2822
3244
  const plain = theme.name === "plain";
2823
3245
  const lore = plain ? PLAIN_LORE : LORE;
2824
- const out = (text2 = "") => {
2825
- write(`${text2}
3246
+ const out = (text3 = "") => {
3247
+ write(`${text3}
2826
3248
  `);
2827
3249
  };
2828
3250
  const icon = (glyph, fallback) => plain ? fallback : glyph;
@@ -2860,16 +3282,16 @@ function createUi(theme, write = stdoutWriter) {
2860
3282
  });
2861
3283
  },
2862
3284
  line: out,
2863
- ok(text2) {
2864
- out(`${icon("\u2705", "[ok]")} ${theme.ok(theme.bold(text2))}`);
3285
+ ok(text3) {
3286
+ out(`${icon("\u2705", "[ok]")} ${theme.ok(theme.bold(text3))}`);
2865
3287
  },
2866
- glitch(text2, details = []) {
2867
- out(`${icon("\u26A0", "[warn]")} ${theme.warn(`${lore.glitch}:`)} ${text2}`);
3288
+ glitch(text3, details = []) {
3289
+ out(`${icon("\u26A0", "[warn]")} ${theme.warn(`${lore.glitch}:`)} ${text3}`);
2868
3290
  for (const detail of details)
2869
3291
  out(` ${theme.dim(detail)}`);
2870
3292
  },
2871
- flatline(text2) {
2872
- out(`${icon("\u2620", "[error]")} ${theme.fail(`${lore.flatlined}:`)} ${text2}`);
3293
+ flatline(text3) {
3294
+ out(`${icon("\u2620", "[error]")} ${theme.fail(`${lore.flatlined}:`)} ${text3}`);
2873
3295
  }
2874
3296
  };
2875
3297
  }
@@ -2878,9 +3300,9 @@ function createUi(theme, write = stdoutWriter) {
2878
3300
  import process5 from "process";
2879
3301
  import pc from "picocolors";
2880
3302
  function rgb(r, g, b) {
2881
- 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`;
2882
3304
  }
2883
- var identity = (text2) => text2;
3305
+ var identity = (text3) => text3;
2884
3306
  function supportsColor() {
2885
3307
  if (process5.env.NO_COLOR != null && process5.env.NO_COLOR !== "")
2886
3308
  return false;
@@ -2913,7 +3335,7 @@ function arasaka() {
2913
3335
  dim: pc.dim,
2914
3336
  ok: tc ? rgb(0, 255, 159) : pc.green,
2915
3337
  warn: pc.yellow,
2916
- fail: (text2) => pc.bold(pc.red(text2)),
3338
+ fail: (text3) => pc.bold(pc.red(text3)),
2917
3339
  bold: pc.bold
2918
3340
  };
2919
3341
  }
@@ -3013,7 +3435,7 @@ var cost = defineCommand({
3013
3435
  json: { type: "boolean", description: "Machine-readable report", default: false }
3014
3436
  },
3015
3437
  run({ args }) {
3016
- const report = costReport(path22.resolve(args.dir));
3438
+ const report = costReport(path21.resolve(args.dir));
3017
3439
  if (args.json) {
3018
3440
  process6.stdout.write(`${JSON.stringify(costJson(report, args.last), null, 2)}
3019
3441
  `);