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