etymd 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -16
- package/README.md +121 -47
- package/dist/{approve-MA4Z3TBT.js → approve-VN4EDBR5.js} +2 -2
- package/dist/{audit-YSALDC2L.js → audit-PHAK5HEW.js} +5 -5
- package/dist/{brief-DPZYSAMC.js → brief-7MWP35UI.js} +2 -2
- package/dist/{chunk-Y6RZRED3.js → chunk-446EZNAQ.js} +1 -1
- package/dist/{chunk-CEO3BXQB.js → chunk-ANFNXFRI.js} +2 -2
- package/dist/{chunk-DBWDMIYO.js → chunk-BUMOGHQA.js} +1 -1
- package/dist/{chunk-IWG77WV3.js → chunk-FNA4R5KT.js} +31 -6
- package/dist/{fleet-4FYZ3GBK.js → chunk-LVR2DPR7.js} +438 -469
- package/dist/{chunk-UFNETE6P.js → chunk-PIJZUDDQ.js} +6 -2
- package/dist/{chunk-2RNQ6OLV.js → chunk-WKP7M2B3.js} +2 -2
- package/dist/cli.js +58 -23
- package/dist/{doctor-Y3DWDEBT.js → doctor-R4QDCAN5.js} +5 -5
- package/dist/fleet-WPYEVHVL.js +489 -0
- package/dist/{gates-IBQ7HCAN.js → gates-3HRGYCYY.js} +7 -4
- package/dist/{generate-KEX75XNG.js → generate-FSJQFQPV.js} +1 -1
- package/dist/index.d.ts +146 -20
- package/dist/index.js +497 -95
- package/dist/{init-H57H2JBE.js → init-6RULX5JZ.js} +3 -3
- package/dist/{premise-VMC3UUIB.js → premise-GFWHSR3K.js} +59 -24
- package/dist/propose-IYNOIWAO.js +252 -0
- package/dist/scan-5SC47FSM.js +5 -0
- package/dist/{scan-XBOHDXAH.js → scan-LLWW4YOU.js} +2 -2
- package/dist/{screen-E4FC7W5N.js → screen-S736DXQ5.js} +56 -24
- package/package.json +4 -4
- package/dist/scan-W4US23MU.js +0 -5
package/dist/index.js
CHANGED
|
@@ -30,7 +30,7 @@ var init_package = __esm({
|
|
|
30
30
|
"package.json"() {
|
|
31
31
|
package_default = {
|
|
32
32
|
name: "etymd",
|
|
33
|
-
version: "0.
|
|
33
|
+
version: "0.17.0",
|
|
34
34
|
description: "Keep your agent instructions true \u2014 verify AGENTS.md, CLAUDE.md, rules, skills, and the task you hand an agent against the actual repo, with drift caught over time and a regression ledger.",
|
|
35
35
|
keywords: [
|
|
36
36
|
"cli",
|
|
@@ -49,11 +49,11 @@ var init_package = __esm({
|
|
|
49
49
|
author: "Alkis Yuv",
|
|
50
50
|
repository: {
|
|
51
51
|
type: "git",
|
|
52
|
-
url: "git+https://github.com/
|
|
52
|
+
url: "git+https://github.com/fleetorders/etymd.git"
|
|
53
53
|
},
|
|
54
|
-
homepage: "https://github.com/
|
|
54
|
+
homepage: "https://github.com/fleetorders/etymd#readme",
|
|
55
55
|
bugs: {
|
|
56
|
-
url: "https://github.com/
|
|
56
|
+
url: "https://github.com/fleetorders/etymd/issues"
|
|
57
57
|
},
|
|
58
58
|
type: "module",
|
|
59
59
|
bin: {
|
|
@@ -1171,7 +1171,7 @@ function generatePreCommitHook(selfBuild = false) {
|
|
|
1171
1171
|
|
|
1172
1172
|
${localHookCall("pre-commit")}
|
|
1173
1173
|
|
|
1174
|
-
# Content screen \u2014 staged file bytes. Refuses to commit environment,
|
|
1174
|
+
# Content screen \u2014 staged file bytes. Refuses to commit environment, guarded-side or identity
|
|
1175
1175
|
# detail into a repo whose history is (or could become) public. The checker and its patterns
|
|
1176
1176
|
# are machine-local by design, so this is a NO-OP wherever no checker is installed: safe to
|
|
1177
1177
|
# commit anywhere, active only where you opted in.
|
|
@@ -1449,9 +1449,13 @@ async function planWorkflow(root, facts, opts) {
|
|
|
1449
1449
|
commitFormat: opts.gateConfig?.commitFormat,
|
|
1450
1450
|
allowWriting: opts.gateConfig?.allowWriting ?? []
|
|
1451
1451
|
};
|
|
1452
|
+
const tier = deriveFailOn(facts, {
|
|
1453
|
+
failOn: gateConfig.failOn,
|
|
1454
|
+
explicit: opts.gateFailOnPinned ?? false
|
|
1455
|
+
});
|
|
1452
1456
|
await add(
|
|
1453
1457
|
".githooks/pre-push",
|
|
1454
|
-
generatePrePushHook(facts, gateConfig, selfBuild),
|
|
1458
|
+
generatePrePushHook(facts, { ...gateConfig, failOn: tier.failOn }, selfBuild),
|
|
1455
1459
|
"Correctness gate (pre-push)",
|
|
1456
1460
|
true
|
|
1457
1461
|
);
|
|
@@ -2762,6 +2766,13 @@ var PLACEHOLDER_PREFIX_RE = /^(?:my|your)-/i;
|
|
|
2762
2766
|
function isPlaceholderClaim(token) {
|
|
2763
2767
|
return token.split("/").some((seg) => PLACEHOLDER_PREFIX_RE.test(seg) || PLACEHOLDER_SEGMENTS.has(seg.toLowerCase()));
|
|
2764
2768
|
}
|
|
2769
|
+
var NAMESPACE_IDENT = "[A-Za-z][A-Za-z0-9-]{0,15}";
|
|
2770
|
+
var NAMESPACE_STOP = new Set(
|
|
2771
|
+
"note notes see warning caution caveat caveats example examples ex eg ie nb ps re per file files path paths hint tip tips todo fixme step steps rule rules ref refs fact facts output outputs input inputs result results summary status overview next prev then thus hence plus goal goals spec specs context".split(" ")
|
|
2772
|
+
);
|
|
2773
|
+
function isNamespace(ident) {
|
|
2774
|
+
return Boolean(ident) && !NAMESPACE_STOP.has(ident.toLowerCase());
|
|
2775
|
+
}
|
|
2765
2776
|
function claimContext(text, index) {
|
|
2766
2777
|
const start = text.lastIndexOf("\n", index) + 1;
|
|
2767
2778
|
const endRaw = text.indexOf("\n", index);
|
|
@@ -2781,8 +2792,9 @@ ${line}`;
|
|
|
2781
2792
|
}
|
|
2782
2793
|
return line;
|
|
2783
2794
|
}
|
|
2784
|
-
function extractPathClaims(text) {
|
|
2795
|
+
function extractPathClaims(text, opts = {}) {
|
|
2785
2796
|
const prospectiveOnly = /* @__PURE__ */ new Map();
|
|
2797
|
+
const namespacedOnly = /* @__PURE__ */ new Map();
|
|
2786
2798
|
const placeholder = /* @__PURE__ */ new Set();
|
|
2787
2799
|
for (const m of text.matchAll(/`([^`\n]+)`/g)) {
|
|
2788
2800
|
const token = m[1].trim();
|
|
@@ -2804,14 +2816,21 @@ function extractPathClaims(text) {
|
|
|
2804
2816
|
}
|
|
2805
2817
|
const prospective2 = CREATION_CONTEXT_RE.test(claimContext(text, m.index ?? 0));
|
|
2806
2818
|
prospectiveOnly.set(claim, (prospectiveOnly.get(claim) ?? true) && prospective2);
|
|
2819
|
+
if (opts.namespaces) {
|
|
2820
|
+
const nsLead = new RegExp(`(${NAMESPACE_IDENT}):[ \\t]*$`).exec(text.slice(0, m.index ?? 0));
|
|
2821
|
+
const prefixed = isNamespace(nsLead?.[1]);
|
|
2822
|
+
namespacedOnly.set(claim, (namespacedOnly.get(claim) ?? true) && prefixed);
|
|
2823
|
+
}
|
|
2807
2824
|
}
|
|
2808
2825
|
const paths = [];
|
|
2809
2826
|
const prospective = [];
|
|
2827
|
+
const namespaced = [];
|
|
2810
2828
|
for (const [claim, only] of prospectiveOnly) {
|
|
2811
2829
|
if (only) prospective.push(claim);
|
|
2830
|
+
else if (namespacedOnly.get(claim)) namespaced.push(claim);
|
|
2812
2831
|
else paths.push(claim);
|
|
2813
2832
|
}
|
|
2814
|
-
return { paths, prospective, placeholder: [...placeholder] };
|
|
2833
|
+
return { paths, prospective, namespaced, placeholder: [...placeholder] };
|
|
2815
2834
|
}
|
|
2816
2835
|
var LOCAL_REF_LEADINS = new Set(
|
|
2817
2836
|
"decision decisions entry entries ruling rulings record records ledger id ids item items see per in of on at by to as is was are were the a an and or but not with under over from via vs than after before since between through against latest newest earliest only also still now supersedes superseded superseding amends amended extends extended cites cited citing adds added adding wrote written writes locked locks closed closes opened opens resolves resolved reopened recorded number numbers".split(" ")
|
|
@@ -2920,7 +2939,9 @@ function emptyCounters() {
|
|
|
2920
2939
|
prospectiveSkipped: 0,
|
|
2921
2940
|
placeholderSkipped: 0,
|
|
2922
2941
|
qualifiedRefsSkipped: 0,
|
|
2923
|
-
unresolvableRefs: 0
|
|
2942
|
+
unresolvableRefs: 0,
|
|
2943
|
+
namespacedSkipped: 0,
|
|
2944
|
+
outsideRepoSkipped: 0
|
|
2924
2945
|
};
|
|
2925
2946
|
}
|
|
2926
2947
|
async function checkTextClaims(env, file, opts, counters) {
|
|
@@ -2958,9 +2979,12 @@ async function checkTextClaims(env, file, opts, counters) {
|
|
|
2958
2979
|
confidence: "high"
|
|
2959
2980
|
});
|
|
2960
2981
|
}
|
|
2961
|
-
const { paths, prospective, placeholder } = extractPathClaims(file.text
|
|
2982
|
+
const { paths, prospective, placeholder, namespaced } = extractPathClaims(file.text, {
|
|
2983
|
+
namespaces: opts.treatNamespacedPrefixes
|
|
2984
|
+
});
|
|
2962
2985
|
counters.prospectiveSkipped += prospective.length;
|
|
2963
2986
|
counters.placeholderSkipped += placeholder.length;
|
|
2987
|
+
counters.namespacedSkipped += namespaced.length;
|
|
2964
2988
|
const missing = [];
|
|
2965
2989
|
for (const claim of paths) {
|
|
2966
2990
|
if (await env.pathResolves(claim)) examined.push({ kind: "path", value: claim, exists: true });
|
|
@@ -2970,6 +2994,11 @@ async function checkTextClaims(env, file, opts, counters) {
|
|
|
2970
2994
|
const gitignored = new Set((ignoredOut ?? "").split("\n").filter(Boolean));
|
|
2971
2995
|
let pathFindings = 0;
|
|
2972
2996
|
for (const claim of missing) {
|
|
2997
|
+
if (opts.rootedFirstSegments && !opts.rootedFirstSegments.has(claim.split("/")[0] ?? claim)) {
|
|
2998
|
+
counters.outsideRepoSkipped += 1;
|
|
2999
|
+
examined.push({ kind: "path", value: claim, exists: null });
|
|
3000
|
+
continue;
|
|
3001
|
+
}
|
|
2973
3002
|
if (gitignored.has(claim)) {
|
|
2974
3003
|
counters.gitignoredSkipped += 1;
|
|
2975
3004
|
examined.push({ kind: "path", value: claim, exists: null });
|
|
@@ -3469,14 +3498,14 @@ function expandTilde(p) {
|
|
|
3469
3498
|
if (p.startsWith("~/")) return path2.join(os.homedir(), p.slice(2));
|
|
3470
3499
|
return p;
|
|
3471
3500
|
}
|
|
3472
|
-
var REBUILD_RECIPE = 'rebuild it beside the manifest: {"machineProfile":"
|
|
3501
|
+
var REBUILD_RECIPE = 'rebuild it beside the manifest: {"machineProfile":"guarded","dirs":{"<name>":"~/path/to/worktree"}}';
|
|
3473
3502
|
var SAFE_NAME = /^[^\s/\\:.][^\s/\\:]*$/;
|
|
3474
3503
|
function rejectUnsafeName(name, where, manifest, manifestFile) {
|
|
3475
3504
|
if (SAFE_NAME.test(name)) return false;
|
|
3476
3505
|
manifest.problems.push({
|
|
3477
3506
|
kind: "bad-shape",
|
|
3478
3507
|
file: manifestFile,
|
|
3479
|
-
detail: `${where} name ${JSON.stringify(name)} is not a single path-safe segment \u2014 entry skipped (names build finding ids and
|
|
3508
|
+
detail: `${where} name ${JSON.stringify(name)} is not a single path-safe segment \u2014 entry skipped (names build finding ids and guarded/<name>/ persistence paths)`
|
|
3480
3509
|
});
|
|
3481
3510
|
return true;
|
|
3482
3511
|
}
|
|
@@ -3517,7 +3546,7 @@ async function readLocal(localPath, problems) {
|
|
|
3517
3546
|
}
|
|
3518
3547
|
}
|
|
3519
3548
|
function loadRegistryEntries(projects, manifest, manifestFile) {
|
|
3520
|
-
const
|
|
3549
|
+
const guardedNames = [];
|
|
3521
3550
|
for (const [i, rawEntry] of projects.entries()) {
|
|
3522
3551
|
const rec = asRecord(rawEntry);
|
|
3523
3552
|
const name = rec ? asString(rec.name) : void 0;
|
|
@@ -3530,8 +3559,8 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
|
|
|
3530
3559
|
continue;
|
|
3531
3560
|
}
|
|
3532
3561
|
if (rejectUnsafeName(name, `projects[${i}]`, manifest, manifestFile)) continue;
|
|
3533
|
-
const profile = rec.profile === "
|
|
3534
|
-
if (rec.profile !== void 0 && rec.profile !== "
|
|
3562
|
+
const profile = rec.profile === "guarded" ? "guarded" : "personal";
|
|
3563
|
+
if (rec.profile !== void 0 && rec.profile !== "guarded" && rec.profile !== "personal") {
|
|
3535
3564
|
manifest.problems.push({
|
|
3536
3565
|
kind: "bad-shape",
|
|
3537
3566
|
file: manifestFile,
|
|
@@ -3569,12 +3598,12 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
|
|
|
3569
3598
|
contract,
|
|
3570
3599
|
links: asStringMap(rec.links)
|
|
3571
3600
|
};
|
|
3572
|
-
if (entry.profile === "
|
|
3573
|
-
|
|
3601
|
+
if (entry.profile === "guarded" || entry.private) {
|
|
3602
|
+
guardedNames.push(name);
|
|
3574
3603
|
if (!manifest.localPresent) {
|
|
3575
3604
|
entry.unresolved = `registry.local.json is absent \u2014 ${REBUILD_RECIPE}`;
|
|
3576
3605
|
} else if (manifest.machineProfile === "personal") {
|
|
3577
|
-
entry.unresolved = '
|
|
3606
|
+
entry.unresolved = 'guarded entries do not resolve under machineProfile "personal" \u2014 absent on this machine by design';
|
|
3578
3607
|
} else {
|
|
3579
3608
|
const dir = manifest.localDirs[name];
|
|
3580
3609
|
if (!dir) {
|
|
@@ -3590,11 +3619,11 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
|
|
|
3590
3619
|
}
|
|
3591
3620
|
manifest.entries.push(entry);
|
|
3592
3621
|
}
|
|
3593
|
-
if (
|
|
3622
|
+
if (guardedNames.length && !manifest.localPresent) {
|
|
3594
3623
|
manifest.problems.push({
|
|
3595
3624
|
kind: "local-missing",
|
|
3596
3625
|
file: path2.basename(manifest.localPath),
|
|
3597
|
-
detail: `${
|
|
3626
|
+
detail: `${guardedNames.length} guarded/private entr${guardedNames.length === 1 ? "y" : "ies"} (${guardedNames.join(", ")}) cannot resolve without it \u2014 ${REBUILD_RECIPE}`
|
|
3598
3627
|
});
|
|
3599
3628
|
}
|
|
3600
3629
|
}
|
|
@@ -3643,7 +3672,7 @@ async function loadFleetManifest(manifestPath) {
|
|
|
3643
3672
|
shape: "registry",
|
|
3644
3673
|
manifestPath: abs,
|
|
3645
3674
|
dir,
|
|
3646
|
-
|
|
3675
|
+
guardedHosts: [],
|
|
3647
3676
|
labels: {},
|
|
3648
3677
|
localDirs: {},
|
|
3649
3678
|
localPath: path2.join(dir, "registry.local.json"),
|
|
@@ -3676,8 +3705,8 @@ async function loadFleetManifest(manifestPath) {
|
|
|
3676
3705
|
manifest.localPresent = local.present;
|
|
3677
3706
|
manifest.localDirs = asStringMap(local.data.dirs);
|
|
3678
3707
|
manifest.labels = asStringMap(local.data.labels);
|
|
3679
|
-
manifest.
|
|
3680
|
-
manifest.machineProfile = local.data.machineProfile === "
|
|
3708
|
+
manifest.guardedHosts = Array.isArray(local.data.guardedHosts) ? local.data.guardedHosts.filter((h) => typeof h === "string") : [];
|
|
3709
|
+
manifest.machineProfile = local.data.machineProfile === "guarded" || local.data.machineProfile === "personal" ? local.data.machineProfile : void 0;
|
|
3681
3710
|
const rootRaw = asString(local.data.root) ?? asString(parsed.root);
|
|
3682
3711
|
if (rootRaw) manifest.root = path2.resolve(dir, expandTilde(rootRaw));
|
|
3683
3712
|
const orientationRec = asRecord(parsed.orientation);
|
|
@@ -3720,10 +3749,136 @@ async function loadFleetManifest(manifestPath) {
|
|
|
3720
3749
|
init_config();
|
|
3721
3750
|
init_facts();
|
|
3722
3751
|
init_util();
|
|
3752
|
+
|
|
3753
|
+
// src/engine/milestones.ts
|
|
3754
|
+
var MILESTONE_COLUMNS = [
|
|
3755
|
+
"id",
|
|
3756
|
+
"milestone",
|
|
3757
|
+
"goal",
|
|
3758
|
+
"status",
|
|
3759
|
+
"next",
|
|
3760
|
+
"effort",
|
|
3761
|
+
"depends-on"
|
|
3762
|
+
];
|
|
3763
|
+
var MILESTONE_GOALS = ["1", "2", "3"];
|
|
3764
|
+
var MILESTONE_STATUSES = ["planned", "active", "blocked", "done"];
|
|
3765
|
+
var MILESTONE_EFFORTS = ["S", "M", "L"];
|
|
3766
|
+
var NONE_MARKERS = /* @__PURE__ */ new Set(["", "\u2014", "-", "\u2013", "none"]);
|
|
3767
|
+
function splitRow(line) {
|
|
3768
|
+
const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
3769
|
+
return trimmed.split("|").map((c) => c.trim());
|
|
3770
|
+
}
|
|
3771
|
+
function isSeparatorRow(line) {
|
|
3772
|
+
const cells = splitRow(line);
|
|
3773
|
+
return cells.length > 0 && cells.every((c) => /^:?-{1,}:?$/.test(c));
|
|
3774
|
+
}
|
|
3775
|
+
function firstTable(lines, startLine) {
|
|
3776
|
+
for (let i = startLine; i < lines.length - 1; i++) {
|
|
3777
|
+
const line = lines[i] ?? "";
|
|
3778
|
+
const next = lines[i + 1] ?? "";
|
|
3779
|
+
if (!line.trim().startsWith("|") || !isSeparatorRow(next)) continue;
|
|
3780
|
+
const header = splitRow(line).map((h) => h.toLowerCase());
|
|
3781
|
+
const rows = [];
|
|
3782
|
+
for (let j = i + 2; j < lines.length; j++) {
|
|
3783
|
+
const body = lines[j] ?? "";
|
|
3784
|
+
if (!body.trim().startsWith("|")) break;
|
|
3785
|
+
rows.push({ line: j + 1, cells: splitRow(body) });
|
|
3786
|
+
}
|
|
3787
|
+
return { header, rows, headerLine: i + 1 };
|
|
3788
|
+
}
|
|
3789
|
+
return null;
|
|
3790
|
+
}
|
|
3791
|
+
function parseList(cell) {
|
|
3792
|
+
if (NONE_MARKERS.has(cell.trim().toLowerCase())) return [];
|
|
3793
|
+
return cell.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3794
|
+
}
|
|
3795
|
+
function checkColumns(table, expected, problems) {
|
|
3796
|
+
const got = table.header.join(" | ");
|
|
3797
|
+
const want = expected.join(" | ");
|
|
3798
|
+
if (got !== want) {
|
|
3799
|
+
problems.push(
|
|
3800
|
+
`line ${table.headerLine}: table columns are \`${got}\`; the standard is \`${want}\``
|
|
3801
|
+
);
|
|
3802
|
+
return false;
|
|
3803
|
+
}
|
|
3804
|
+
return true;
|
|
3805
|
+
}
|
|
3806
|
+
function checkVocabulary(line, field, value, allowed, problems) {
|
|
3807
|
+
if (allowed.includes(value)) return true;
|
|
3808
|
+
problems.push(
|
|
3809
|
+
`line ${line}: ${field} is \`${value}\`; allowed: ${allowed.map((a) => `\`${a}\``).join(", ")}`
|
|
3810
|
+
);
|
|
3811
|
+
return false;
|
|
3812
|
+
}
|
|
3813
|
+
function parseMilestones(text) {
|
|
3814
|
+
const problems = [];
|
|
3815
|
+
const lines = text.split(/\r?\n/);
|
|
3816
|
+
const headingIdx = lines.findIndex((l) => /^#\s/.test(l));
|
|
3817
|
+
if (headingIdx === -1 || lines[headingIdx]?.trim() !== "# Milestones") {
|
|
3818
|
+
problems.push(
|
|
3819
|
+
headingIdx === -1 ? "no heading: the file must start with `# Milestones`" : `line ${headingIdx + 1}: first heading is \`${lines[headingIdx]?.trim()}\`; it must be \`# Milestones\``
|
|
3820
|
+
);
|
|
3821
|
+
}
|
|
3822
|
+
const table = firstTable(lines, headingIdx === -1 ? 0 : headingIdx + 1);
|
|
3823
|
+
if (!table) {
|
|
3824
|
+
problems.push(
|
|
3825
|
+
`no milestones table: expected \`| ${MILESTONE_COLUMNS.join(" | ")} |\` after the heading`
|
|
3826
|
+
);
|
|
3827
|
+
return { rows: [], problems };
|
|
3828
|
+
}
|
|
3829
|
+
if (!checkColumns(table, MILESTONE_COLUMNS, problems)) return { rows: [], problems };
|
|
3830
|
+
const rows = [];
|
|
3831
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3832
|
+
for (const { line, cells } of table.rows) {
|
|
3833
|
+
if (cells.length !== MILESTONE_COLUMNS.length) {
|
|
3834
|
+
problems.push(`line ${line}: ${cells.length} cells, expected ${MILESTONE_COLUMNS.length}`);
|
|
3835
|
+
continue;
|
|
3836
|
+
}
|
|
3837
|
+
const [id, milestone, goal, status, next, effort, deps] = cells;
|
|
3838
|
+
let ok = true;
|
|
3839
|
+
if (!/^M\d+$/.test(id)) {
|
|
3840
|
+
problems.push(`line ${line}: id \`${id}\` is not \`M<number>\``);
|
|
3841
|
+
ok = false;
|
|
3842
|
+
} else if (ids.has(id)) {
|
|
3843
|
+
problems.push(`line ${line}: id \`${id}\` is used twice`);
|
|
3844
|
+
ok = false;
|
|
3845
|
+
}
|
|
3846
|
+
if (!milestone) {
|
|
3847
|
+
problems.push(`line ${line}: milestone title is empty`);
|
|
3848
|
+
ok = false;
|
|
3849
|
+
}
|
|
3850
|
+
if (!next) {
|
|
3851
|
+
problems.push(`line ${line}: next step is empty \u2014 a milestone without a next step is a wish`);
|
|
3852
|
+
ok = false;
|
|
3853
|
+
}
|
|
3854
|
+
ok = checkVocabulary(line, "goal", goal, MILESTONE_GOALS, problems) && ok;
|
|
3855
|
+
ok = checkVocabulary(line, "status", status, MILESTONE_STATUSES, problems) && ok;
|
|
3856
|
+
ok = checkVocabulary(line, "effort", effort, MILESTONE_EFFORTS, problems) && ok;
|
|
3857
|
+
if (!ok) continue;
|
|
3858
|
+
ids.add(id);
|
|
3859
|
+
rows.push({
|
|
3860
|
+
id,
|
|
3861
|
+
milestone,
|
|
3862
|
+
goal,
|
|
3863
|
+
status,
|
|
3864
|
+
next,
|
|
3865
|
+
effort,
|
|
3866
|
+
dependsOn: parseList(deps)
|
|
3867
|
+
});
|
|
3868
|
+
}
|
|
3869
|
+
for (const r of rows) {
|
|
3870
|
+
for (const d of r.dependsOn) {
|
|
3871
|
+
if (!ids.has(d)) problems.push(`${r.id} depends on \`${d}\`, which is not in the table`);
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
return { rows, problems };
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
// src/engine/fleet.ts
|
|
3723
3878
|
var pExecFile3 = promisify(execFile);
|
|
3724
3879
|
var FLEET_LENS = "fleet-manifest";
|
|
3725
3880
|
var FLEET_JSON_SCHEMA = "fleet-experimental-0.2";
|
|
3726
|
-
var
|
|
3881
|
+
var GUARDED_WALL_ARTIFACTS = ["PROJECT_CONTEXT.md", "DECISIONS.md"];
|
|
3727
3882
|
function recurringClasses(projects) {
|
|
3728
3883
|
const byClass = /* @__PURE__ */ new Map();
|
|
3729
3884
|
for (const p of projects) {
|
|
@@ -3756,12 +3911,12 @@ function finding3(id, tier, claim, evidence, why, action) {
|
|
|
3756
3911
|
confidence: "high"
|
|
3757
3912
|
};
|
|
3758
3913
|
}
|
|
3759
|
-
function
|
|
3760
|
-
const
|
|
3761
|
-
const root = path2.resolve(
|
|
3762
|
-
if (path2.dirname(root) !==
|
|
3914
|
+
function guardedPersistenceRoot(manifest, name) {
|
|
3915
|
+
const guardedDir = path2.join(manifest.dir, "guarded");
|
|
3916
|
+
const root = path2.resolve(guardedDir, name);
|
|
3917
|
+
if (path2.dirname(root) !== guardedDir || root === guardedDir) {
|
|
3763
3918
|
throw new Error(
|
|
3764
|
-
`entry name ${JSON.stringify(name)} escapes the
|
|
3919
|
+
`entry name ${JSON.stringify(name)} escapes the guarded persistence zone \u2014 refusing to resolve a ledger root for it`
|
|
3765
3920
|
);
|
|
3766
3921
|
}
|
|
3767
3922
|
return root;
|
|
@@ -3794,7 +3949,7 @@ async function realpathOr(p) {
|
|
|
3794
3949
|
return path2.resolve(p);
|
|
3795
3950
|
}
|
|
3796
3951
|
}
|
|
3797
|
-
function
|
|
3952
|
+
function emailMatchesGuardedHosts(email, hosts) {
|
|
3798
3953
|
const domain = email.split("@")[1]?.toLowerCase();
|
|
3799
3954
|
if (!domain) return false;
|
|
3800
3955
|
return hosts.some((h) => {
|
|
@@ -3845,7 +4000,7 @@ async function checkManifest(manifest) {
|
|
|
3845
4000
|
);
|
|
3846
4001
|
}
|
|
3847
4002
|
seen.add(entry.name);
|
|
3848
|
-
if (entry.profile !== "
|
|
4003
|
+
if (entry.profile !== "guarded" && !entry.private && !entry.trust) {
|
|
3849
4004
|
const bad = entry.trustRaw;
|
|
3850
4005
|
findings.push(
|
|
3851
4006
|
finding3(
|
|
@@ -3883,14 +4038,14 @@ async function checkManifest(manifest) {
|
|
|
3883
4038
|
)
|
|
3884
4039
|
);
|
|
3885
4040
|
}
|
|
3886
|
-
if (entry.unresolved && manifest.localPresent && manifest.machineProfile !== "personal" && (entry.profile === "
|
|
4041
|
+
if (entry.unresolved && manifest.localPresent && manifest.machineProfile !== "personal" && (entry.profile === "guarded" || entry.private)) {
|
|
3887
4042
|
findings.push(
|
|
3888
4043
|
finding3(
|
|
3889
4044
|
`${FLEET_LENS}/dangling-dir:${entry.name}`,
|
|
3890
4045
|
"gap",
|
|
3891
4046
|
`\`${entry.name}\` has no usable directory mapping`,
|
|
3892
4047
|
[`${localFile}: ${entry.unresolved}`],
|
|
3893
|
-
"An unmapped
|
|
4048
|
+
"An unmapped guarded entry silently drops out of every sweep on the machine that most needs it.",
|
|
3894
4049
|
`Add a \`dirs\` mapping for \`${entry.name}\` in ${localFile}.`
|
|
3895
4050
|
)
|
|
3896
4051
|
);
|
|
@@ -3954,28 +4109,28 @@ async function checkManifest(manifest) {
|
|
|
3954
4109
|
);
|
|
3955
4110
|
}
|
|
3956
4111
|
if (manifest.machineProfile === "personal") {
|
|
3957
|
-
const
|
|
3958
|
-
if (
|
|
4112
|
+
const guardedCount = manifest.entries.filter((e) => e.profile === "guarded" || e.private).length;
|
|
4113
|
+
if (guardedCount) {
|
|
3959
4114
|
disclosures.push(
|
|
3960
|
-
`machineProfile "personal": ${
|
|
4115
|
+
`machineProfile "personal": ${guardedCount} guarded entr${guardedCount === 1 ? "y" : "ies"} deliberately absent on this machine \u2014 disclosed, not dangling.`
|
|
3961
4116
|
);
|
|
3962
4117
|
}
|
|
3963
4118
|
}
|
|
3964
4119
|
return { findings, disclosures };
|
|
3965
4120
|
}
|
|
3966
|
-
async function
|
|
4121
|
+
async function checkGuardedWallArtifacts(entries, findings) {
|
|
3967
4122
|
for (const entry of entries) {
|
|
3968
|
-
if (entry.profile !== "
|
|
3969
|
-
for (const artifact of
|
|
4123
|
+
if (entry.profile !== "guarded" || !entry.resolvedRoot) continue;
|
|
4124
|
+
for (const artifact of GUARDED_WALL_ARTIFACTS) {
|
|
3970
4125
|
if (await pathExists(path2.join(entry.resolvedRoot, artifact))) {
|
|
3971
4126
|
findings.push(
|
|
3972
4127
|
finding3(
|
|
3973
|
-
`${FLEET_LENS}/
|
|
4128
|
+
`${FLEET_LENS}/guarded-artifact-in-repo:${entry.name}:${artifact}`,
|
|
3974
4129
|
"risk",
|
|
3975
|
-
`
|
|
4130
|
+
`guarded entry \`${entry.name}\` carries ${artifact} inside its worktree`,
|
|
3976
4131
|
[`${entry.resolvedRoot}/${artifact}`],
|
|
3977
|
-
"Agent artifacts for
|
|
3978
|
-
`Move it to the fleet's
|
|
4132
|
+
"Agent artifacts for guarded repos live outside the repo by policy \u2014 an in-repo copy is one push away from colleagues' eyes and will diverge from the manifest-side truth.",
|
|
4133
|
+
`Move it to the fleet's guarded/${entry.name}/ zone and leave a pointer if the harness needs one.`
|
|
3979
4134
|
)
|
|
3980
4135
|
);
|
|
3981
4136
|
}
|
|
@@ -3983,9 +4138,9 @@ async function checkCorpWallArtifacts(entries, findings) {
|
|
|
3983
4138
|
}
|
|
3984
4139
|
}
|
|
3985
4140
|
async function checkCoverage(manifest, findings, disclosures) {
|
|
3986
|
-
if (!manifest.
|
|
4141
|
+
if (!manifest.guardedHosts.length) {
|
|
3987
4142
|
disclosures.push(
|
|
3988
|
-
"Coverage check skipped \u2014 no
|
|
4143
|
+
"Coverage check skipped \u2014 no guardedHosts in the local manifest, so a guarded remote cannot be recognized."
|
|
3989
4144
|
);
|
|
3990
4145
|
return;
|
|
3991
4146
|
}
|
|
@@ -4006,16 +4161,16 @@ async function checkCoverage(manifest, findings, disclosures) {
|
|
|
4006
4161
|
if (!await pathExists(path2.join(dir, ".git"))) continue;
|
|
4007
4162
|
const remotes = await git(dir, ["config", "--get-regexp", "^remote\\..*\\.url"]);
|
|
4008
4163
|
if (!remotes) continue;
|
|
4009
|
-
const
|
|
4010
|
-
if (
|
|
4164
|
+
const guardedRemote = manifest.guardedHosts.find((h) => remotes.includes(h));
|
|
4165
|
+
if (guardedRemote) {
|
|
4011
4166
|
findings.push(
|
|
4012
4167
|
finding3(
|
|
4013
|
-
`${FLEET_LENS}/unregistered-
|
|
4168
|
+
`${FLEET_LENS}/unregistered-guarded-remote:${d.name}`,
|
|
4014
4169
|
"risk",
|
|
4015
|
-
`\`${d.name}\` under the fleet root has a
|
|
4016
|
-
[`${dir}: remote matches ${
|
|
4017
|
-
"The wall is only as good as its census \u2014 an unregistered
|
|
4018
|
-
"Register it as a
|
|
4170
|
+
`\`${d.name}\` under the fleet root has a guarded remote but is not registered`,
|
|
4171
|
+
[`${dir}: remote matches ${guardedRemote}`],
|
|
4172
|
+
"The wall is only as good as its census \u2014 an unregistered guarded checkout is guarded material no sweep, wall check, or transfer plan knows exists.",
|
|
4173
|
+
"Register it as a guarded entry (opaque alias + local dirs mapping) or move it out of the fleet root."
|
|
4019
4174
|
)
|
|
4020
4175
|
);
|
|
4021
4176
|
}
|
|
@@ -4063,7 +4218,7 @@ async function checkHygieneNeedles(manifest, findings, disclosures) {
|
|
|
4063
4218
|
[
|
|
4064
4219
|
...Object.values(manifest.labels),
|
|
4065
4220
|
...Object.values(manifest.localDirs).map((d) => path2.basename(expandNeedleDir(d))),
|
|
4066
|
-
...manifest.
|
|
4221
|
+
...manifest.guardedHosts
|
|
4067
4222
|
].filter((n) => n.length >= 3)
|
|
4068
4223
|
)
|
|
4069
4224
|
];
|
|
@@ -4090,7 +4245,7 @@ async function checkHygieneNeedles(manifest, findings, disclosures) {
|
|
|
4090
4245
|
"risk",
|
|
4091
4246
|
`${file} in public-repo \`${entry.name}\` carries a needle from the fleet's private local manifest`,
|
|
4092
4247
|
[`${file}: contains "${needle}"`],
|
|
4093
|
-
"A public repo that names a private label,
|
|
4248
|
+
"A public repo that names a private label, guarded directory, or guarded host joins the public and guarded identities for anyone who greps.",
|
|
4094
4249
|
"Scrub the needle from the tracked file (rewrite history if it already shipped)."
|
|
4095
4250
|
)
|
|
4096
4251
|
);
|
|
@@ -4101,10 +4256,10 @@ async function checkHygieneNeedles(manifest, findings, disclosures) {
|
|
|
4101
4256
|
function expandNeedleDir(dir) {
|
|
4102
4257
|
return dir.replace(/\/+$/, "");
|
|
4103
4258
|
}
|
|
4104
|
-
async function
|
|
4105
|
-
if (!manifest.
|
|
4259
|
+
async function checkGuardedEmails(manifest, findings, disclosures) {
|
|
4260
|
+
if (!manifest.guardedHosts.length) {
|
|
4106
4261
|
disclosures.push(
|
|
4107
|
-
"
|
|
4262
|
+
"Guarded-email check skipped \u2014 no guardedHosts in the local manifest, so a guarded domain cannot be recognized."
|
|
4108
4263
|
);
|
|
4109
4264
|
return;
|
|
4110
4265
|
}
|
|
@@ -4113,21 +4268,23 @@ async function checkCorpEmails(manifest, findings, disclosures) {
|
|
|
4113
4268
|
const emails = await git(entry.resolvedRoot, ["log", "-30", "--format=%ae"]);
|
|
4114
4269
|
if (emails === null) {
|
|
4115
4270
|
disclosures.push(
|
|
4116
|
-
`
|
|
4271
|
+
`Guarded-email check could not run in \`${entry.name}\` (no readable commit history) \u2014 undetermined, not clean.`
|
|
4117
4272
|
);
|
|
4118
4273
|
continue;
|
|
4119
4274
|
}
|
|
4120
4275
|
const lines = emails.split("\n").filter(Boolean);
|
|
4121
|
-
const matched = [
|
|
4276
|
+
const matched = [
|
|
4277
|
+
...new Set(lines.filter((e) => emailMatchesGuardedHosts(e, manifest.guardedHosts)))
|
|
4278
|
+
];
|
|
4122
4279
|
if (matched.length) {
|
|
4123
4280
|
const count = lines.filter((e) => matched.includes(e)).length;
|
|
4124
4281
|
findings.push(
|
|
4125
4282
|
finding3(
|
|
4126
|
-
`${FLEET_LENS}/
|
|
4283
|
+
`${FLEET_LENS}/guarded-email:${entry.name}`,
|
|
4127
4284
|
"risk",
|
|
4128
|
-
`recent commits on personal entry \`${entry.name}\` are authored with a
|
|
4285
|
+
`recent commits on personal entry \`${entry.name}\` are authored with a guarded-host email`,
|
|
4129
4286
|
[`${count} of the last ${lines.length} commits: ${matched.join(", ")}`],
|
|
4130
|
-
"A
|
|
4287
|
+
"A guarded email in a personal repo's history ties the two identities together permanently \u2014 and silently, since the machine's global git identity is usually the guarded one.",
|
|
4131
4288
|
"Set the repo-local identity to the personal one and amend the unpushed commits."
|
|
4132
4289
|
)
|
|
4133
4290
|
);
|
|
@@ -4141,7 +4298,7 @@ async function checkGateDrift(manifest, findings, disclosures) {
|
|
|
4141
4298
|
for (const entry of manifest.entries) {
|
|
4142
4299
|
const root = entry.resolvedRoot;
|
|
4143
4300
|
if (!root || !await isDirectory(root)) continue;
|
|
4144
|
-
if (entry.profile === "
|
|
4301
|
+
if (entry.profile === "guarded" || entry.private) continue;
|
|
4145
4302
|
if (entry.gates === "none") {
|
|
4146
4303
|
disclosures.push(`${entry.name}: gates declared \`none\` \u2014 deliberately absent, not missing`);
|
|
4147
4304
|
continue;
|
|
@@ -4154,8 +4311,13 @@ async function checkGateDrift(manifest, findings, disclosures) {
|
|
|
4154
4311
|
);
|
|
4155
4312
|
continue;
|
|
4156
4313
|
}
|
|
4157
|
-
const { config } = await readConfig2(root);
|
|
4158
|
-
const planned = (await planWorkflow2(root, facts, {
|
|
4314
|
+
const { config, explicit } = await readConfig2(root);
|
|
4315
|
+
const planned = (await planWorkflow2(root, facts, {
|
|
4316
|
+
agents: false,
|
|
4317
|
+
gates: true,
|
|
4318
|
+
gateConfig: config.gates,
|
|
4319
|
+
gateFailOnPinned: explicit.gatesFailOn
|
|
4320
|
+
})).filter((f) => f.executable);
|
|
4159
4321
|
const missing = planned.filter((f) => !f.exists).map((f) => f.path);
|
|
4160
4322
|
const differing = planned.filter((f) => f.exists && f.differs);
|
|
4161
4323
|
const stale = differing.filter((f) => f.drift !== "edited").map((f) => f.path);
|
|
@@ -4194,11 +4356,11 @@ async function checkGateDrift(manifest, findings, disclosures) {
|
|
|
4194
4356
|
async function collectWallFindings(manifest) {
|
|
4195
4357
|
const findings = [];
|
|
4196
4358
|
const disclosures = [];
|
|
4197
|
-
await
|
|
4359
|
+
await checkGuardedWallArtifacts(manifest.entries, findings);
|
|
4198
4360
|
await checkCoverage(manifest, findings, disclosures);
|
|
4199
4361
|
await checkManifestRepoMachinePaths(manifest, findings, disclosures);
|
|
4200
4362
|
await checkHygieneNeedles(manifest, findings, disclosures);
|
|
4201
|
-
await
|
|
4363
|
+
await checkGuardedEmails(manifest, findings, disclosures);
|
|
4202
4364
|
await checkGateDrift(manifest, findings, disclosures);
|
|
4203
4365
|
return { findings, disclosures };
|
|
4204
4366
|
}
|
|
@@ -4264,13 +4426,13 @@ async function sweepEntry(manifest, entry, opts) {
|
|
|
4264
4426
|
);
|
|
4265
4427
|
}
|
|
4266
4428
|
}
|
|
4267
|
-
const
|
|
4268
|
-
const optedIn = !
|
|
4429
|
+
const guarded = entry.profile === "guarded";
|
|
4430
|
+
const optedIn = !guarded && await pathExists(path2.join(root, ETYMD_DIR));
|
|
4269
4431
|
const result = await runAudit(root, {
|
|
4270
4432
|
kind: opts.kind,
|
|
4271
|
-
persistLedger: !
|
|
4272
|
-
readOnlyRoot:
|
|
4273
|
-
ledgerRoot:
|
|
4433
|
+
persistLedger: !guarded && opts.persistLedgers === true && optedIn,
|
|
4434
|
+
readOnlyRoot: guarded,
|
|
4435
|
+
ledgerRoot: guarded ? guardedPersistenceRoot(manifest, entry.name) : void 0,
|
|
4274
4436
|
stateBudgets: stateBudgetsFor(entry),
|
|
4275
4437
|
upstreamRemote
|
|
4276
4438
|
});
|
|
@@ -4278,11 +4440,43 @@ async function sweepEntry(manifest, entry, opts) {
|
|
|
4278
4440
|
if (entry.contract.placement === "none") {
|
|
4279
4441
|
project.findings = project.findings.filter((f) => !f.id.endsWith("/no-contract"));
|
|
4280
4442
|
}
|
|
4443
|
+
const milestonesFile = entry.contract.milestones;
|
|
4444
|
+
if (milestonesFile && milestonesFile !== "none") {
|
|
4445
|
+
const text = await readText(path2.join(root, milestonesFile));
|
|
4446
|
+
if (text === null) {
|
|
4447
|
+
project.findings.push(
|
|
4448
|
+
finding3(
|
|
4449
|
+
`${FLEET_LENS}/milestones-missing:${entry.name}`,
|
|
4450
|
+
"gap",
|
|
4451
|
+
`\`${entry.name}\` declares milestones at \`${milestonesFile}\` and the file is absent`,
|
|
4452
|
+
[
|
|
4453
|
+
`${path2.basename(manifest.manifestPath)}: ${entry.name}.contract.milestones = ${milestonesFile}`
|
|
4454
|
+
],
|
|
4455
|
+
"The fleet board rolls every project's plan up from this file; a declared file that is absent is a row the board cannot fill.",
|
|
4456
|
+
'Create it (heading `# Milestones`, then the standard table), or declare `milestones: "none"` if this project deliberately carries no plan.'
|
|
4457
|
+
)
|
|
4458
|
+
);
|
|
4459
|
+
} else {
|
|
4460
|
+
const doc = parseMilestones(text);
|
|
4461
|
+
doc.problems.forEach((problem, i) => {
|
|
4462
|
+
project.findings.push(
|
|
4463
|
+
finding3(
|
|
4464
|
+
`${FLEET_LENS}/milestones-shape:${entry.name}:${i + 1}`,
|
|
4465
|
+
"gap",
|
|
4466
|
+
`\`${entry.name}\` ${milestonesFile}: ${problem}`,
|
|
4467
|
+
[`${milestonesFile}`],
|
|
4468
|
+
"A milestones file the board cannot parse is a plan the fleet cannot see.",
|
|
4469
|
+
`Fix the table to the standard shape: \`| ${MILESTONE_COLUMNS.join(" | ")} |\`.`
|
|
4470
|
+
)
|
|
4471
|
+
);
|
|
4472
|
+
});
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4281
4475
|
for (const f of project.findings) project.counts[f.tier] += 1;
|
|
4282
4476
|
project.stateAgeDays = stateAgeDays(result.facts);
|
|
4283
4477
|
project.staleAfterDays = result.config.config.state.staleAfterDays;
|
|
4284
4478
|
for (const [key, value] of Object.entries(entry.contract)) {
|
|
4285
|
-
if (key === "placement" || !value) continue;
|
|
4479
|
+
if (key === "placement" || key === "milestones" || !value) continue;
|
|
4286
4480
|
const exists = await pathExists(path2.join(root, value.replace(/#.*$/, "")));
|
|
4287
4481
|
project.disclosures.push(
|
|
4288
4482
|
`contract override \`${key}: ${value}\` \u2014 registered, existence-checked only (${exists ? "present" : "MISSING"})`
|
|
@@ -4325,6 +4519,179 @@ async function sweepFleet(manifest, opts = {}) {
|
|
|
4325
4519
|
};
|
|
4326
4520
|
}
|
|
4327
4521
|
|
|
4522
|
+
// src/engine/propose.ts
|
|
4523
|
+
var PROPOSAL_SCHEMA = "proposal/1";
|
|
4524
|
+
var RUBRIC_CRITERIA = ["severity", "economy", "confidence", "breadth"];
|
|
4525
|
+
function parseRubric(text, file) {
|
|
4526
|
+
const lines = [];
|
|
4527
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4528
|
+
const rows = text.split(/\r?\n/);
|
|
4529
|
+
for (const [i, raw] of rows.entries()) {
|
|
4530
|
+
const line = raw.trim();
|
|
4531
|
+
if (!line || line.startsWith("#")) continue;
|
|
4532
|
+
const at = i + 1;
|
|
4533
|
+
const refuse = (why) => new Error(
|
|
4534
|
+
`${file}:${at} \`${raw.trim()}\` \u2014 ${why} (criteria: ${RUBRIC_CRITERIA.join(", ")})`
|
|
4535
|
+
);
|
|
4536
|
+
const sep = line.indexOf(":");
|
|
4537
|
+
if (sep < 0) throw refuse("expected `criterion: <weight>`");
|
|
4538
|
+
const criterion = line.slice(0, sep).trim();
|
|
4539
|
+
const weightRaw = line.slice(sep + 1).trim();
|
|
4540
|
+
if (!RUBRIC_CRITERIA.includes(criterion)) {
|
|
4541
|
+
throw refuse(`unknown criterion \`${criterion}\``);
|
|
4542
|
+
}
|
|
4543
|
+
if (!/^\d+$/.test(weightRaw) || Number(weightRaw) < 1) {
|
|
4544
|
+
throw refuse(`weight \`${weightRaw}\` is not a positive integer`);
|
|
4545
|
+
}
|
|
4546
|
+
const known = criterion;
|
|
4547
|
+
if (seen.has(known)) {
|
|
4548
|
+
throw new Error(
|
|
4549
|
+
`${file}:${at} \`${raw.trim()}\` \u2014 criterion \`${criterion}\` is already weighted on line ${seen.get(known)}; one line per criterion`
|
|
4550
|
+
);
|
|
4551
|
+
}
|
|
4552
|
+
seen.set(known, at);
|
|
4553
|
+
lines.push({ criterion: known, weight: Number(weightRaw) });
|
|
4554
|
+
}
|
|
4555
|
+
if (!lines.length) {
|
|
4556
|
+
throw new Error(`${file} carries no criterion lines \u2014 a rubric with no criteria scores nothing`);
|
|
4557
|
+
}
|
|
4558
|
+
return { lines };
|
|
4559
|
+
}
|
|
4560
|
+
var TIER_VALUE = { risk: 3, gap: 2, polish: 1 };
|
|
4561
|
+
var EFFORT_VALUE = { S: 3, M: 2, L: 1 };
|
|
4562
|
+
var CONFIDENCE_VALUE = { high: 3, medium: 2, low: 1 };
|
|
4563
|
+
function findingSubject(project, f) {
|
|
4564
|
+
return {
|
|
4565
|
+
id: `${project.name}:${f.id}`,
|
|
4566
|
+
kind: "finding",
|
|
4567
|
+
classId: f.id.split(":")[0] ?? f.id,
|
|
4568
|
+
tier: f.tier,
|
|
4569
|
+
effort: f.effort,
|
|
4570
|
+
confidence: f.confidence,
|
|
4571
|
+
projects: [project.name],
|
|
4572
|
+
action: f.action ?? "",
|
|
4573
|
+
evidence: f.evidence
|
|
4574
|
+
};
|
|
4575
|
+
}
|
|
4576
|
+
function classSubject(classId, members) {
|
|
4577
|
+
const pick = (order, at, dir) => {
|
|
4578
|
+
const values = members.map((m) => order[at(m.finding)]);
|
|
4579
|
+
const value = dir === 1 ? Math.max(...values) : Math.min(...values);
|
|
4580
|
+
return Object.entries(order).find(([, v]) => v === value)[0];
|
|
4581
|
+
};
|
|
4582
|
+
const actions = [...new Set(members.map((m) => m.finding.action).filter(Boolean))].sort();
|
|
4583
|
+
return {
|
|
4584
|
+
id: `class:${classId}`,
|
|
4585
|
+
kind: "class",
|
|
4586
|
+
classId,
|
|
4587
|
+
tier: pick(TIER_VALUE, (f) => f.tier, 1),
|
|
4588
|
+
effort: pick(EFFORT_VALUE, (f) => f.effort, -1),
|
|
4589
|
+
confidence: pick(CONFIDENCE_VALUE, (f) => f.confidence, -1),
|
|
4590
|
+
projects: [...new Set(members.map((m) => m.project.name))].sort(),
|
|
4591
|
+
action: actions.join("; "),
|
|
4592
|
+
evidence: members.flatMap((m) => m.finding.evidence)
|
|
4593
|
+
};
|
|
4594
|
+
}
|
|
4595
|
+
var CRITERION_VALUE = {
|
|
4596
|
+
severity: (s) => TIER_VALUE[s.tier],
|
|
4597
|
+
economy: (s) => EFFORT_VALUE[s.effort],
|
|
4598
|
+
confidence: (s) => CONFIDENCE_VALUE[s.confidence],
|
|
4599
|
+
breadth: (s) => Math.min(3, s.projects.length)
|
|
4600
|
+
};
|
|
4601
|
+
var FILENAME_RE = /^[^\s:]+\.[A-Za-z0-9]+$/;
|
|
4602
|
+
function extractFiles(evidence) {
|
|
4603
|
+
const files = /* @__PURE__ */ new Set();
|
|
4604
|
+
for (const line of evidence) {
|
|
4605
|
+
const token = line.split(/[\s:]/)[0];
|
|
4606
|
+
if (token && (token.includes("/") || FILENAME_RE.test(token))) files.add(token);
|
|
4607
|
+
}
|
|
4608
|
+
return [...files].sort();
|
|
4609
|
+
}
|
|
4610
|
+
function extractGates(files) {
|
|
4611
|
+
return files.filter((f) => f.startsWith(".githooks/") || f.startsWith(".github/workflows/"));
|
|
4612
|
+
}
|
|
4613
|
+
function reversibility(files) {
|
|
4614
|
+
if (!files.length) return "undetermined";
|
|
4615
|
+
const generated = (f) => f.startsWith(".etymd/") || f.startsWith(".githooks/");
|
|
4616
|
+
return files.every(generated) ? "regenerable" : "git-reversible";
|
|
4617
|
+
}
|
|
4618
|
+
function toProposal(s, rubric) {
|
|
4619
|
+
const matched = [];
|
|
4620
|
+
let score = 0;
|
|
4621
|
+
for (const line of rubric.lines) {
|
|
4622
|
+
const value = CRITERION_VALUE[line.criterion](s);
|
|
4623
|
+
score += line.weight * value;
|
|
4624
|
+
if (value >= 2) matched.push({ criterion: line.criterion, weight: line.weight, value });
|
|
4625
|
+
}
|
|
4626
|
+
const files = extractFiles(s.evidence);
|
|
4627
|
+
return {
|
|
4628
|
+
id: s.id,
|
|
4629
|
+
schema: PROPOSAL_SCHEMA,
|
|
4630
|
+
class: s.classId,
|
|
4631
|
+
kind: s.kind,
|
|
4632
|
+
projects: s.projects,
|
|
4633
|
+
action: s.action,
|
|
4634
|
+
effort: s.effort,
|
|
4635
|
+
confidence: s.confidence,
|
|
4636
|
+
score,
|
|
4637
|
+
matched,
|
|
4638
|
+
implications: {
|
|
4639
|
+
projects: s.projects,
|
|
4640
|
+
files,
|
|
4641
|
+
gates: extractGates(files),
|
|
4642
|
+
reversibility: reversibility(files)
|
|
4643
|
+
}
|
|
4644
|
+
};
|
|
4645
|
+
}
|
|
4646
|
+
function buildProposals(sweep, rubricName, rubric) {
|
|
4647
|
+
const disclosures = [];
|
|
4648
|
+
const personal = sweep.projects.filter((p) => p.profile === "personal");
|
|
4649
|
+
const guarded = sweep.projects.filter((p) => p.profile !== "personal");
|
|
4650
|
+
if (guarded.length) {
|
|
4651
|
+
disclosures.push(
|
|
4652
|
+
`guarded entries excluded from proposals: ${guarded.map((p) => p.name).sort().join(", ")}`
|
|
4653
|
+
);
|
|
4654
|
+
}
|
|
4655
|
+
const notAudited = personal.filter((p) => !p.resolvedRoot || p.unresolved);
|
|
4656
|
+
if (notAudited.length) {
|
|
4657
|
+
disclosures.push(
|
|
4658
|
+
`not audited, so not proposed: ${notAudited.map((p) => p.name).sort().join(", ")}`
|
|
4659
|
+
);
|
|
4660
|
+
}
|
|
4661
|
+
const subjects = [];
|
|
4662
|
+
for (const project of personal) {
|
|
4663
|
+
for (const f of project.findings) {
|
|
4664
|
+
if (f.kind === "improvement") subjects.push(findingSubject(project, f));
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
const byClass = /* @__PURE__ */ new Map();
|
|
4668
|
+
for (const project of personal) {
|
|
4669
|
+
for (const f of project.findings) {
|
|
4670
|
+
const classId = f.id.split(":")[0] ?? f.id;
|
|
4671
|
+
const entry = byClass.get(classId) ?? [];
|
|
4672
|
+
entry.push({ project, finding: f });
|
|
4673
|
+
byClass.set(classId, entry);
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
for (const [classId, members] of byClass) {
|
|
4677
|
+
if (new Set(members.map((m) => m.project.name)).size >= 2) {
|
|
4678
|
+
subjects.push(classSubject(classId, members));
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
const proposals = subjects.map((s) => toProposal(s, rubric)).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4682
|
+
return {
|
|
4683
|
+
schema: PROPOSAL_SCHEMA,
|
|
4684
|
+
manifest: sweep.manifest,
|
|
4685
|
+
rubric: rubricName,
|
|
4686
|
+
criteria: rubric.lines,
|
|
4687
|
+
proposals,
|
|
4688
|
+
disclosures: [
|
|
4689
|
+
...disclosures,
|
|
4690
|
+
"implications.files are path-shaped tokens extracted from finding evidence \u2014 evidence lines carrying no path are not represented there"
|
|
4691
|
+
]
|
|
4692
|
+
};
|
|
4693
|
+
}
|
|
4694
|
+
|
|
4328
4695
|
// src/engine/premise.ts
|
|
4329
4696
|
init_facts();
|
|
4330
4697
|
init_scan();
|
|
@@ -4347,7 +4714,8 @@ function promoteBareTokens(text, ctx) {
|
|
|
4347
4714
|
bareInvocations: 0,
|
|
4348
4715
|
proseScripts: 0,
|
|
4349
4716
|
hostnameLike: 0,
|
|
4350
|
-
unrootedDirs: 0
|
|
4717
|
+
unrootedDirs: 0,
|
|
4718
|
+
namespaced: 0
|
|
4351
4719
|
};
|
|
4352
4720
|
const promoted = text.split(CODE_SPAN_RE).map((segment, i) => i % 2 === 1 ? segment : promoteProse(segment, ctx, skips)).join("");
|
|
4353
4721
|
return { text: promoted, skips };
|
|
@@ -4366,28 +4734,44 @@ function promoteProse(segment, ctx, skips) {
|
|
|
4366
4734
|
}
|
|
4367
4735
|
return `\`${core}\`${trail}`;
|
|
4368
4736
|
});
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4737
|
+
const parts = withCommands.split(/(\s+)/);
|
|
4738
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
4739
|
+
parts[i] = promoteToken(parts[i], i >= 2 ? parts[i - 2] : "", ctx, skips);
|
|
4740
|
+
}
|
|
4741
|
+
return parts.join("");
|
|
4742
|
+
}
|
|
4743
|
+
function namespaceOf(core, prev) {
|
|
4744
|
+
const ns = new RegExp(`^(${NAMESPACE_IDENT}):`).exec(core)?.[1] ?? new RegExp(`^(${NAMESPACE_IDENT}):$`).exec(prev)?.[1];
|
|
4745
|
+
return ns && !NAMESPACE_STOP.has(ns.toLowerCase()) ? ns : null;
|
|
4746
|
+
}
|
|
4747
|
+
function promoteToken(token, prev, ctx, skips) {
|
|
4748
|
+
const m = WRAP_RE.exec(token);
|
|
4749
|
+
if (!m) return token;
|
|
4750
|
+
const [, lead = "", core = "", trail = ""] = m;
|
|
4751
|
+
if (!core || core.includes("://")) return token;
|
|
4752
|
+
const namespace = namespaceOf(core, prev);
|
|
4753
|
+
const body = namespace && core.startsWith(`${namespace}:`) ? core.slice(namespace.length + 1) : core;
|
|
4754
|
+
if (!PATH_TOKEN_RE.test(body)) return token;
|
|
4755
|
+
const first = body.slice(0, body.indexOf("/"));
|
|
4756
|
+
const hostSuffix = HOSTNAME_RE.exec(first)?.[1]?.toLowerCase();
|
|
4757
|
+
if (hostSuffix && !KNOWN_EXTENSIONS.has(hostSuffix)) {
|
|
4758
|
+
skips.hostnameLike += 1;
|
|
4759
|
+
return token;
|
|
4760
|
+
}
|
|
4761
|
+
if (body.endsWith("/")) {
|
|
4762
|
+
if (!ctx.rootedDirs.has(first)) {
|
|
4763
|
+
skips.unrootedDirs += 1;
|
|
4378
4764
|
return token;
|
|
4379
4765
|
}
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
skips.unrootedDirs += 1;
|
|
4383
|
-
return token;
|
|
4384
|
-
}
|
|
4385
|
-
return `${lead}\`${core}\`${trail}`;
|
|
4386
|
-
}
|
|
4387
|
-
const ext = core.toLowerCase().match(/\.([a-z0-9]{1,8})$/)?.[1];
|
|
4766
|
+
} else {
|
|
4767
|
+
const ext = body.toLowerCase().match(/\.([a-z0-9]{1,8})$/)?.[1];
|
|
4388
4768
|
if (!ext || !KNOWN_EXTENSIONS.has(ext)) return token;
|
|
4389
|
-
|
|
4390
|
-
|
|
4769
|
+
}
|
|
4770
|
+
if (namespace) {
|
|
4771
|
+
skips.namespaced += 1;
|
|
4772
|
+
return token;
|
|
4773
|
+
}
|
|
4774
|
+
return `${lead}\`${body}\`${trail}`;
|
|
4391
4775
|
}
|
|
4392
4776
|
async function listRootedDirs(env) {
|
|
4393
4777
|
const dirs = /* @__PURE__ */ new Set();
|
|
@@ -4405,7 +4789,8 @@ async function runPremise(opts) {
|
|
|
4405
4789
|
const facts = await scanProject(root);
|
|
4406
4790
|
const env = await buildTruthEnv(root, facts);
|
|
4407
4791
|
const counters = emptyCounters();
|
|
4408
|
-
const
|
|
4792
|
+
const rootedDirs = await listRootedDirs(env);
|
|
4793
|
+
const promoted = promoteBareTokens(task, { rootedDirs });
|
|
4409
4794
|
const text = { path: TASK_LABEL, text: promoted.text };
|
|
4410
4795
|
const findings = [];
|
|
4411
4796
|
const disclosures = [];
|
|
@@ -4418,6 +4803,8 @@ async function runPremise(opts) {
|
|
|
4418
4803
|
subject: "The task",
|
|
4419
4804
|
missingPathTier: "risk",
|
|
4420
4805
|
maxPathFindings: MAX_PATH_FINDINGS,
|
|
4806
|
+
rootedFirstSegments: rootedDirs,
|
|
4807
|
+
treatNamespacedPrefixes: true,
|
|
4421
4808
|
whyCommand: "The task is built on a command that does not exist \u2014 an agent will run it and fail, or quietly substitute something else and report success.",
|
|
4422
4809
|
actionCommand: "Fix the task before handing it over: name the real script, or restore it.",
|
|
4423
4810
|
whyPath: "The task is ABOUT this path. Acting on it means editing, testing, or reasoning about something that is not there \u2014 the wrong problem, solved precisely.",
|
|
@@ -4474,6 +4861,21 @@ async function runPremise(opts) {
|
|
|
4474
4861
|
`${skips.unrootedDirs} slash-joined phrase(s) ending in \`/\` start with no directory that exists here (e.g. \`input/output/\`) \u2014 read as prose; skipped, not flagged. Backtick one to have it checked.`
|
|
4475
4862
|
);
|
|
4476
4863
|
}
|
|
4864
|
+
if (skips.namespaced) {
|
|
4865
|
+
disclosures.push(
|
|
4866
|
+
`${skips.namespaced} path mention(s) in prose carry a namespace prefix (\`pc:src/x.ts\`, \`lk: src/x.ts\`) \u2014 another repo's tree, never this one; skipped, not flagged.`
|
|
4867
|
+
);
|
|
4868
|
+
}
|
|
4869
|
+
if (counters.namespacedSkipped) {
|
|
4870
|
+
disclosures.push(
|
|
4871
|
+
`${counters.namespacedSkipped} backticked path mention(s) sit directly after a namespace prefix (\`pc: \`src/x.ts\`\`) \u2014 another repo's tree, not this one; skipped, not flagged.`
|
|
4872
|
+
);
|
|
4873
|
+
}
|
|
4874
|
+
if (counters.outsideRepoSkipped) {
|
|
4875
|
+
disclosures.push(
|
|
4876
|
+
`${counters.outsideRepoSkipped} path(s) the task names start at no directory of this repo \u2014 typical of a path quoted from another repository or a scratchpad clone; outside this repo, not missing here. Skipped, not flagged.`
|
|
4877
|
+
);
|
|
4878
|
+
}
|
|
4477
4879
|
if (counters.unverifiableCommands) {
|
|
4478
4880
|
disclosures.push(
|
|
4479
4881
|
`node_modules is not installed \u2014 ${counters.unverifiableCommands} command(s) matching no package script could not be checked against installed binaries; skipped, not flagged.`
|
|
@@ -4594,4 +4996,4 @@ wrong problem.
|
|
|
4594
4996
|
init_version();
|
|
4595
4997
|
init_version2();
|
|
4596
4998
|
|
|
4597
|
-
export { CONFIG_FILE, DECISIONS_FORMAT_MARKER, DEFAULT_CONFIG, EXTRACTION_THRESHOLD, FLEET_JSON_SCHEMA, FLEET_LENS, LENSES, NAME, PACK_VERSION, PREMISE_BRIEF_FILE, PREMISE_LENS, TOTAL_BUDGET_WORDS, VERSION, applyFiles, baselineCarriesMachinePath, baselinePath, buildGateInventory, buildTruthEnv, cacheFactsPath, checkDecisionRefs, checkDocRefs, checkManifest, checkTextClaims, collectWallFindings, configPath, contextEconomyLens,
|
|
4999
|
+
export { CONFIG_FILE, DECISIONS_FORMAT_MARKER, DEFAULT_CONFIG, EXTRACTION_THRESHOLD, FLEET_JSON_SCHEMA, FLEET_LENS, LENSES, NAME, PACK_VERSION, PREMISE_BRIEF_FILE, PREMISE_LENS, PROPOSAL_SCHEMA, RUBRIC_CRITERIA, TOTAL_BUDGET_WORDS, VERSION, applyFiles, baselineCarriesMachinePath, baselinePath, buildGateInventory, buildProposals, buildTruthEnv, cacheFactsPath, checkDecisionRefs, checkDocRefs, checkManifest, checkTextClaims, collectWallFindings, configPath, contextEconomyLens, deriveProfile, emptyCounters, expandTilde, extractCommandClaims, extractPathClaims, gateIntegrityLens, guardedPersistenceRoot, instructionTruthLens, isAlwaysAppliedCursorRule, listInstructionFiles, loadDecisionLedger, loadFleetManifest, measureContext, parseRubric, planWorkflow, promoteBareTokens, rankFindings, readBaseline, readCachedFacts, readConfig, readLedger, reconcileLedger, renderBrief, runAudit, runPremise, scanProject, stateFreshnessLens, sweepFleet, withoutMachinePath, writeBaseline, writeCachedFacts, writeLedger };
|