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