etymd 0.15.0 → 0.16.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 +38 -16
- package/README.md +103 -47
- package/dist/{approve-MA4Z3TBT.js → approve-K5FFTPA4.js} +2 -2
- package/dist/{audit-YSALDC2L.js → audit-HBNXNGD6.js} +5 -5
- package/dist/{brief-DPZYSAMC.js → brief-DOQ2DQEG.js} +2 -2
- package/dist/{fleet-4FYZ3GBK.js → chunk-ANXGT7V3.js} +431 -467
- package/dist/{chunk-Y6RZRED3.js → chunk-EJM47PIF.js} +1 -1
- package/dist/{chunk-CEO3BXQB.js → chunk-FKDT4RBP.js} +2 -2
- package/dist/{chunk-UFNETE6P.js → chunk-OKJ3RGJB.js} +1 -1
- package/dist/{chunk-2RNQ6OLV.js → chunk-UV3YIBDW.js} +2 -2
- package/dist/{chunk-DBWDMIYO.js → chunk-UYCGP5BS.js} +1 -1
- package/dist/{chunk-IWG77WV3.js → chunk-Y4E6ERCM.js} +31 -6
- package/dist/cli.js +57 -22
- package/dist/{doctor-Y3DWDEBT.js → doctor-X6XAGKV7.js} +5 -5
- package/dist/fleet-UEFLKIRI.js +489 -0
- package/dist/{gates-IBQ7HCAN.js → gates-NVWWKMT2.js} +3 -3
- package/dist/{generate-KEX75XNG.js → generate-YEARCSHW.js} +1 -1
- package/dist/index.d.ts +138 -20
- package/dist/index.js +485 -92
- package/dist/{init-H57H2JBE.js → init-Z5O2WVRJ.js} +3 -3
- package/dist/{premise-VMC3UUIB.js → premise-4QRU7LRI.js} +59 -24
- package/dist/propose-IKE4CUIM.js +252 -0
- package/dist/scan-64Z6SNYF.js +5 -0
- package/dist/{scan-XBOHDXAH.js → scan-H3NF5IV5.js} +2 -2
- 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.16.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.
|
|
@@ -2762,6 +2762,13 @@ var PLACEHOLDER_PREFIX_RE = /^(?:my|your)-/i;
|
|
|
2762
2762
|
function isPlaceholderClaim(token) {
|
|
2763
2763
|
return token.split("/").some((seg) => PLACEHOLDER_PREFIX_RE.test(seg) || PLACEHOLDER_SEGMENTS.has(seg.toLowerCase()));
|
|
2764
2764
|
}
|
|
2765
|
+
var NAMESPACE_IDENT = "[A-Za-z][A-Za-z0-9-]{0,15}";
|
|
2766
|
+
var NAMESPACE_STOP = new Set(
|
|
2767
|
+
"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(" ")
|
|
2768
|
+
);
|
|
2769
|
+
function isNamespace(ident) {
|
|
2770
|
+
return Boolean(ident) && !NAMESPACE_STOP.has(ident.toLowerCase());
|
|
2771
|
+
}
|
|
2765
2772
|
function claimContext(text, index) {
|
|
2766
2773
|
const start = text.lastIndexOf("\n", index) + 1;
|
|
2767
2774
|
const endRaw = text.indexOf("\n", index);
|
|
@@ -2781,8 +2788,9 @@ ${line}`;
|
|
|
2781
2788
|
}
|
|
2782
2789
|
return line;
|
|
2783
2790
|
}
|
|
2784
|
-
function extractPathClaims(text) {
|
|
2791
|
+
function extractPathClaims(text, opts = {}) {
|
|
2785
2792
|
const prospectiveOnly = /* @__PURE__ */ new Map();
|
|
2793
|
+
const namespacedOnly = /* @__PURE__ */ new Map();
|
|
2786
2794
|
const placeholder = /* @__PURE__ */ new Set();
|
|
2787
2795
|
for (const m of text.matchAll(/`([^`\n]+)`/g)) {
|
|
2788
2796
|
const token = m[1].trim();
|
|
@@ -2804,14 +2812,21 @@ function extractPathClaims(text) {
|
|
|
2804
2812
|
}
|
|
2805
2813
|
const prospective2 = CREATION_CONTEXT_RE.test(claimContext(text, m.index ?? 0));
|
|
2806
2814
|
prospectiveOnly.set(claim, (prospectiveOnly.get(claim) ?? true) && prospective2);
|
|
2815
|
+
if (opts.namespaces) {
|
|
2816
|
+
const nsLead = new RegExp(`(${NAMESPACE_IDENT}):[ \\t]*$`).exec(text.slice(0, m.index ?? 0));
|
|
2817
|
+
const prefixed = isNamespace(nsLead?.[1]);
|
|
2818
|
+
namespacedOnly.set(claim, (namespacedOnly.get(claim) ?? true) && prefixed);
|
|
2819
|
+
}
|
|
2807
2820
|
}
|
|
2808
2821
|
const paths = [];
|
|
2809
2822
|
const prospective = [];
|
|
2823
|
+
const namespaced = [];
|
|
2810
2824
|
for (const [claim, only] of prospectiveOnly) {
|
|
2811
2825
|
if (only) prospective.push(claim);
|
|
2826
|
+
else if (namespacedOnly.get(claim)) namespaced.push(claim);
|
|
2812
2827
|
else paths.push(claim);
|
|
2813
2828
|
}
|
|
2814
|
-
return { paths, prospective, placeholder: [...placeholder] };
|
|
2829
|
+
return { paths, prospective, namespaced, placeholder: [...placeholder] };
|
|
2815
2830
|
}
|
|
2816
2831
|
var LOCAL_REF_LEADINS = new Set(
|
|
2817
2832
|
"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 +2935,9 @@ function emptyCounters() {
|
|
|
2920
2935
|
prospectiveSkipped: 0,
|
|
2921
2936
|
placeholderSkipped: 0,
|
|
2922
2937
|
qualifiedRefsSkipped: 0,
|
|
2923
|
-
unresolvableRefs: 0
|
|
2938
|
+
unresolvableRefs: 0,
|
|
2939
|
+
namespacedSkipped: 0,
|
|
2940
|
+
outsideRepoSkipped: 0
|
|
2924
2941
|
};
|
|
2925
2942
|
}
|
|
2926
2943
|
async function checkTextClaims(env, file, opts, counters) {
|
|
@@ -2958,9 +2975,12 @@ async function checkTextClaims(env, file, opts, counters) {
|
|
|
2958
2975
|
confidence: "high"
|
|
2959
2976
|
});
|
|
2960
2977
|
}
|
|
2961
|
-
const { paths, prospective, placeholder } = extractPathClaims(file.text
|
|
2978
|
+
const { paths, prospective, placeholder, namespaced } = extractPathClaims(file.text, {
|
|
2979
|
+
namespaces: opts.treatNamespacedPrefixes
|
|
2980
|
+
});
|
|
2962
2981
|
counters.prospectiveSkipped += prospective.length;
|
|
2963
2982
|
counters.placeholderSkipped += placeholder.length;
|
|
2983
|
+
counters.namespacedSkipped += namespaced.length;
|
|
2964
2984
|
const missing = [];
|
|
2965
2985
|
for (const claim of paths) {
|
|
2966
2986
|
if (await env.pathResolves(claim)) examined.push({ kind: "path", value: claim, exists: true });
|
|
@@ -2970,6 +2990,11 @@ async function checkTextClaims(env, file, opts, counters) {
|
|
|
2970
2990
|
const gitignored = new Set((ignoredOut ?? "").split("\n").filter(Boolean));
|
|
2971
2991
|
let pathFindings = 0;
|
|
2972
2992
|
for (const claim of missing) {
|
|
2993
|
+
if (opts.rootedFirstSegments && !opts.rootedFirstSegments.has(claim.split("/")[0] ?? claim)) {
|
|
2994
|
+
counters.outsideRepoSkipped += 1;
|
|
2995
|
+
examined.push({ kind: "path", value: claim, exists: null });
|
|
2996
|
+
continue;
|
|
2997
|
+
}
|
|
2973
2998
|
if (gitignored.has(claim)) {
|
|
2974
2999
|
counters.gitignoredSkipped += 1;
|
|
2975
3000
|
examined.push({ kind: "path", value: claim, exists: null });
|
|
@@ -3469,14 +3494,14 @@ function expandTilde(p) {
|
|
|
3469
3494
|
if (p.startsWith("~/")) return path2.join(os.homedir(), p.slice(2));
|
|
3470
3495
|
return p;
|
|
3471
3496
|
}
|
|
3472
|
-
var REBUILD_RECIPE = 'rebuild it beside the manifest: {"machineProfile":"
|
|
3497
|
+
var REBUILD_RECIPE = 'rebuild it beside the manifest: {"machineProfile":"guarded","dirs":{"<name>":"~/path/to/worktree"}}';
|
|
3473
3498
|
var SAFE_NAME = /^[^\s/\\:.][^\s/\\:]*$/;
|
|
3474
3499
|
function rejectUnsafeName(name, where, manifest, manifestFile) {
|
|
3475
3500
|
if (SAFE_NAME.test(name)) return false;
|
|
3476
3501
|
manifest.problems.push({
|
|
3477
3502
|
kind: "bad-shape",
|
|
3478
3503
|
file: manifestFile,
|
|
3479
|
-
detail: `${where} name ${JSON.stringify(name)} is not a single path-safe segment \u2014 entry skipped (names build finding ids and
|
|
3504
|
+
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
3505
|
});
|
|
3481
3506
|
return true;
|
|
3482
3507
|
}
|
|
@@ -3517,7 +3542,7 @@ async function readLocal(localPath, problems) {
|
|
|
3517
3542
|
}
|
|
3518
3543
|
}
|
|
3519
3544
|
function loadRegistryEntries(projects, manifest, manifestFile) {
|
|
3520
|
-
const
|
|
3545
|
+
const guardedNames = [];
|
|
3521
3546
|
for (const [i, rawEntry] of projects.entries()) {
|
|
3522
3547
|
const rec = asRecord(rawEntry);
|
|
3523
3548
|
const name = rec ? asString(rec.name) : void 0;
|
|
@@ -3530,8 +3555,8 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
|
|
|
3530
3555
|
continue;
|
|
3531
3556
|
}
|
|
3532
3557
|
if (rejectUnsafeName(name, `projects[${i}]`, manifest, manifestFile)) continue;
|
|
3533
|
-
const profile = rec.profile === "
|
|
3534
|
-
if (rec.profile !== void 0 && rec.profile !== "
|
|
3558
|
+
const profile = rec.profile === "guarded" ? "guarded" : "personal";
|
|
3559
|
+
if (rec.profile !== void 0 && rec.profile !== "guarded" && rec.profile !== "personal") {
|
|
3535
3560
|
manifest.problems.push({
|
|
3536
3561
|
kind: "bad-shape",
|
|
3537
3562
|
file: manifestFile,
|
|
@@ -3569,12 +3594,12 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
|
|
|
3569
3594
|
contract,
|
|
3570
3595
|
links: asStringMap(rec.links)
|
|
3571
3596
|
};
|
|
3572
|
-
if (entry.profile === "
|
|
3573
|
-
|
|
3597
|
+
if (entry.profile === "guarded" || entry.private) {
|
|
3598
|
+
guardedNames.push(name);
|
|
3574
3599
|
if (!manifest.localPresent) {
|
|
3575
3600
|
entry.unresolved = `registry.local.json is absent \u2014 ${REBUILD_RECIPE}`;
|
|
3576
3601
|
} else if (manifest.machineProfile === "personal") {
|
|
3577
|
-
entry.unresolved = '
|
|
3602
|
+
entry.unresolved = 'guarded entries do not resolve under machineProfile "personal" \u2014 absent on this machine by design';
|
|
3578
3603
|
} else {
|
|
3579
3604
|
const dir = manifest.localDirs[name];
|
|
3580
3605
|
if (!dir) {
|
|
@@ -3590,11 +3615,11 @@ function loadRegistryEntries(projects, manifest, manifestFile) {
|
|
|
3590
3615
|
}
|
|
3591
3616
|
manifest.entries.push(entry);
|
|
3592
3617
|
}
|
|
3593
|
-
if (
|
|
3618
|
+
if (guardedNames.length && !manifest.localPresent) {
|
|
3594
3619
|
manifest.problems.push({
|
|
3595
3620
|
kind: "local-missing",
|
|
3596
3621
|
file: path2.basename(manifest.localPath),
|
|
3597
|
-
detail: `${
|
|
3622
|
+
detail: `${guardedNames.length} guarded/private entr${guardedNames.length === 1 ? "y" : "ies"} (${guardedNames.join(", ")}) cannot resolve without it \u2014 ${REBUILD_RECIPE}`
|
|
3598
3623
|
});
|
|
3599
3624
|
}
|
|
3600
3625
|
}
|
|
@@ -3643,7 +3668,7 @@ async function loadFleetManifest(manifestPath) {
|
|
|
3643
3668
|
shape: "registry",
|
|
3644
3669
|
manifestPath: abs,
|
|
3645
3670
|
dir,
|
|
3646
|
-
|
|
3671
|
+
guardedHosts: [],
|
|
3647
3672
|
labels: {},
|
|
3648
3673
|
localDirs: {},
|
|
3649
3674
|
localPath: path2.join(dir, "registry.local.json"),
|
|
@@ -3676,8 +3701,8 @@ async function loadFleetManifest(manifestPath) {
|
|
|
3676
3701
|
manifest.localPresent = local.present;
|
|
3677
3702
|
manifest.localDirs = asStringMap(local.data.dirs);
|
|
3678
3703
|
manifest.labels = asStringMap(local.data.labels);
|
|
3679
|
-
manifest.
|
|
3680
|
-
manifest.machineProfile = local.data.machineProfile === "
|
|
3704
|
+
manifest.guardedHosts = Array.isArray(local.data.guardedHosts) ? local.data.guardedHosts.filter((h) => typeof h === "string") : [];
|
|
3705
|
+
manifest.machineProfile = local.data.machineProfile === "guarded" || local.data.machineProfile === "personal" ? local.data.machineProfile : void 0;
|
|
3681
3706
|
const rootRaw = asString(local.data.root) ?? asString(parsed.root);
|
|
3682
3707
|
if (rootRaw) manifest.root = path2.resolve(dir, expandTilde(rootRaw));
|
|
3683
3708
|
const orientationRec = asRecord(parsed.orientation);
|
|
@@ -3720,10 +3745,136 @@ async function loadFleetManifest(manifestPath) {
|
|
|
3720
3745
|
init_config();
|
|
3721
3746
|
init_facts();
|
|
3722
3747
|
init_util();
|
|
3748
|
+
|
|
3749
|
+
// src/engine/milestones.ts
|
|
3750
|
+
var MILESTONE_COLUMNS = [
|
|
3751
|
+
"id",
|
|
3752
|
+
"milestone",
|
|
3753
|
+
"goal",
|
|
3754
|
+
"status",
|
|
3755
|
+
"next",
|
|
3756
|
+
"effort",
|
|
3757
|
+
"depends-on"
|
|
3758
|
+
];
|
|
3759
|
+
var MILESTONE_GOALS = ["1", "2", "3"];
|
|
3760
|
+
var MILESTONE_STATUSES = ["planned", "active", "blocked", "done"];
|
|
3761
|
+
var MILESTONE_EFFORTS = ["S", "M", "L"];
|
|
3762
|
+
var NONE_MARKERS = /* @__PURE__ */ new Set(["", "\u2014", "-", "\u2013", "none"]);
|
|
3763
|
+
function splitRow(line) {
|
|
3764
|
+
const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
3765
|
+
return trimmed.split("|").map((c) => c.trim());
|
|
3766
|
+
}
|
|
3767
|
+
function isSeparatorRow(line) {
|
|
3768
|
+
const cells = splitRow(line);
|
|
3769
|
+
return cells.length > 0 && cells.every((c) => /^:?-{1,}:?$/.test(c));
|
|
3770
|
+
}
|
|
3771
|
+
function firstTable(lines, startLine) {
|
|
3772
|
+
for (let i = startLine; i < lines.length - 1; i++) {
|
|
3773
|
+
const line = lines[i] ?? "";
|
|
3774
|
+
const next = lines[i + 1] ?? "";
|
|
3775
|
+
if (!line.trim().startsWith("|") || !isSeparatorRow(next)) continue;
|
|
3776
|
+
const header = splitRow(line).map((h) => h.toLowerCase());
|
|
3777
|
+
const rows = [];
|
|
3778
|
+
for (let j = i + 2; j < lines.length; j++) {
|
|
3779
|
+
const body = lines[j] ?? "";
|
|
3780
|
+
if (!body.trim().startsWith("|")) break;
|
|
3781
|
+
rows.push({ line: j + 1, cells: splitRow(body) });
|
|
3782
|
+
}
|
|
3783
|
+
return { header, rows, headerLine: i + 1 };
|
|
3784
|
+
}
|
|
3785
|
+
return null;
|
|
3786
|
+
}
|
|
3787
|
+
function parseList(cell) {
|
|
3788
|
+
if (NONE_MARKERS.has(cell.trim().toLowerCase())) return [];
|
|
3789
|
+
return cell.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3790
|
+
}
|
|
3791
|
+
function checkColumns(table, expected, problems) {
|
|
3792
|
+
const got = table.header.join(" | ");
|
|
3793
|
+
const want = expected.join(" | ");
|
|
3794
|
+
if (got !== want) {
|
|
3795
|
+
problems.push(
|
|
3796
|
+
`line ${table.headerLine}: table columns are \`${got}\`; the standard is \`${want}\``
|
|
3797
|
+
);
|
|
3798
|
+
return false;
|
|
3799
|
+
}
|
|
3800
|
+
return true;
|
|
3801
|
+
}
|
|
3802
|
+
function checkVocabulary(line, field, value, allowed, problems) {
|
|
3803
|
+
if (allowed.includes(value)) return true;
|
|
3804
|
+
problems.push(
|
|
3805
|
+
`line ${line}: ${field} is \`${value}\`; allowed: ${allowed.map((a) => `\`${a}\``).join(", ")}`
|
|
3806
|
+
);
|
|
3807
|
+
return false;
|
|
3808
|
+
}
|
|
3809
|
+
function parseMilestones(text) {
|
|
3810
|
+
const problems = [];
|
|
3811
|
+
const lines = text.split(/\r?\n/);
|
|
3812
|
+
const headingIdx = lines.findIndex((l) => /^#\s/.test(l));
|
|
3813
|
+
if (headingIdx === -1 || lines[headingIdx]?.trim() !== "# Milestones") {
|
|
3814
|
+
problems.push(
|
|
3815
|
+
headingIdx === -1 ? "no heading: the file must start with `# Milestones`" : `line ${headingIdx + 1}: first heading is \`${lines[headingIdx]?.trim()}\`; it must be \`# Milestones\``
|
|
3816
|
+
);
|
|
3817
|
+
}
|
|
3818
|
+
const table = firstTable(lines, headingIdx === -1 ? 0 : headingIdx + 1);
|
|
3819
|
+
if (!table) {
|
|
3820
|
+
problems.push(
|
|
3821
|
+
`no milestones table: expected \`| ${MILESTONE_COLUMNS.join(" | ")} |\` after the heading`
|
|
3822
|
+
);
|
|
3823
|
+
return { rows: [], problems };
|
|
3824
|
+
}
|
|
3825
|
+
if (!checkColumns(table, MILESTONE_COLUMNS, problems)) return { rows: [], problems };
|
|
3826
|
+
const rows = [];
|
|
3827
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3828
|
+
for (const { line, cells } of table.rows) {
|
|
3829
|
+
if (cells.length !== MILESTONE_COLUMNS.length) {
|
|
3830
|
+
problems.push(`line ${line}: ${cells.length} cells, expected ${MILESTONE_COLUMNS.length}`);
|
|
3831
|
+
continue;
|
|
3832
|
+
}
|
|
3833
|
+
const [id, milestone, goal, status, next, effort, deps] = cells;
|
|
3834
|
+
let ok = true;
|
|
3835
|
+
if (!/^M\d+$/.test(id)) {
|
|
3836
|
+
problems.push(`line ${line}: id \`${id}\` is not \`M<number>\``);
|
|
3837
|
+
ok = false;
|
|
3838
|
+
} else if (ids.has(id)) {
|
|
3839
|
+
problems.push(`line ${line}: id \`${id}\` is used twice`);
|
|
3840
|
+
ok = false;
|
|
3841
|
+
}
|
|
3842
|
+
if (!milestone) {
|
|
3843
|
+
problems.push(`line ${line}: milestone title is empty`);
|
|
3844
|
+
ok = false;
|
|
3845
|
+
}
|
|
3846
|
+
if (!next) {
|
|
3847
|
+
problems.push(`line ${line}: next step is empty \u2014 a milestone without a next step is a wish`);
|
|
3848
|
+
ok = false;
|
|
3849
|
+
}
|
|
3850
|
+
ok = checkVocabulary(line, "goal", goal, MILESTONE_GOALS, problems) && ok;
|
|
3851
|
+
ok = checkVocabulary(line, "status", status, MILESTONE_STATUSES, problems) && ok;
|
|
3852
|
+
ok = checkVocabulary(line, "effort", effort, MILESTONE_EFFORTS, problems) && ok;
|
|
3853
|
+
if (!ok) continue;
|
|
3854
|
+
ids.add(id);
|
|
3855
|
+
rows.push({
|
|
3856
|
+
id,
|
|
3857
|
+
milestone,
|
|
3858
|
+
goal,
|
|
3859
|
+
status,
|
|
3860
|
+
next,
|
|
3861
|
+
effort,
|
|
3862
|
+
dependsOn: parseList(deps)
|
|
3863
|
+
});
|
|
3864
|
+
}
|
|
3865
|
+
for (const r of rows) {
|
|
3866
|
+
for (const d of r.dependsOn) {
|
|
3867
|
+
if (!ids.has(d)) problems.push(`${r.id} depends on \`${d}\`, which is not in the table`);
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
return { rows, problems };
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
// src/engine/fleet.ts
|
|
3723
3874
|
var pExecFile3 = promisify(execFile);
|
|
3724
3875
|
var FLEET_LENS = "fleet-manifest";
|
|
3725
3876
|
var FLEET_JSON_SCHEMA = "fleet-experimental-0.2";
|
|
3726
|
-
var
|
|
3877
|
+
var GUARDED_WALL_ARTIFACTS = ["PROJECT_CONTEXT.md", "DECISIONS.md"];
|
|
3727
3878
|
function recurringClasses(projects) {
|
|
3728
3879
|
const byClass = /* @__PURE__ */ new Map();
|
|
3729
3880
|
for (const p of projects) {
|
|
@@ -3756,12 +3907,12 @@ function finding3(id, tier, claim, evidence, why, action) {
|
|
|
3756
3907
|
confidence: "high"
|
|
3757
3908
|
};
|
|
3758
3909
|
}
|
|
3759
|
-
function
|
|
3760
|
-
const
|
|
3761
|
-
const root = path2.resolve(
|
|
3762
|
-
if (path2.dirname(root) !==
|
|
3910
|
+
function guardedPersistenceRoot(manifest, name) {
|
|
3911
|
+
const guardedDir = path2.join(manifest.dir, "guarded");
|
|
3912
|
+
const root = path2.resolve(guardedDir, name);
|
|
3913
|
+
if (path2.dirname(root) !== guardedDir || root === guardedDir) {
|
|
3763
3914
|
throw new Error(
|
|
3764
|
-
`entry name ${JSON.stringify(name)} escapes the
|
|
3915
|
+
`entry name ${JSON.stringify(name)} escapes the guarded persistence zone \u2014 refusing to resolve a ledger root for it`
|
|
3765
3916
|
);
|
|
3766
3917
|
}
|
|
3767
3918
|
return root;
|
|
@@ -3794,7 +3945,7 @@ async function realpathOr(p) {
|
|
|
3794
3945
|
return path2.resolve(p);
|
|
3795
3946
|
}
|
|
3796
3947
|
}
|
|
3797
|
-
function
|
|
3948
|
+
function emailMatchesGuardedHosts(email, hosts) {
|
|
3798
3949
|
const domain = email.split("@")[1]?.toLowerCase();
|
|
3799
3950
|
if (!domain) return false;
|
|
3800
3951
|
return hosts.some((h) => {
|
|
@@ -3845,7 +3996,7 @@ async function checkManifest(manifest) {
|
|
|
3845
3996
|
);
|
|
3846
3997
|
}
|
|
3847
3998
|
seen.add(entry.name);
|
|
3848
|
-
if (entry.profile !== "
|
|
3999
|
+
if (entry.profile !== "guarded" && !entry.private && !entry.trust) {
|
|
3849
4000
|
const bad = entry.trustRaw;
|
|
3850
4001
|
findings.push(
|
|
3851
4002
|
finding3(
|
|
@@ -3883,14 +4034,14 @@ async function checkManifest(manifest) {
|
|
|
3883
4034
|
)
|
|
3884
4035
|
);
|
|
3885
4036
|
}
|
|
3886
|
-
if (entry.unresolved && manifest.localPresent && manifest.machineProfile !== "personal" && (entry.profile === "
|
|
4037
|
+
if (entry.unresolved && manifest.localPresent && manifest.machineProfile !== "personal" && (entry.profile === "guarded" || entry.private)) {
|
|
3887
4038
|
findings.push(
|
|
3888
4039
|
finding3(
|
|
3889
4040
|
`${FLEET_LENS}/dangling-dir:${entry.name}`,
|
|
3890
4041
|
"gap",
|
|
3891
4042
|
`\`${entry.name}\` has no usable directory mapping`,
|
|
3892
4043
|
[`${localFile}: ${entry.unresolved}`],
|
|
3893
|
-
"An unmapped
|
|
4044
|
+
"An unmapped guarded entry silently drops out of every sweep on the machine that most needs it.",
|
|
3894
4045
|
`Add a \`dirs\` mapping for \`${entry.name}\` in ${localFile}.`
|
|
3895
4046
|
)
|
|
3896
4047
|
);
|
|
@@ -3954,28 +4105,28 @@ async function checkManifest(manifest) {
|
|
|
3954
4105
|
);
|
|
3955
4106
|
}
|
|
3956
4107
|
if (manifest.machineProfile === "personal") {
|
|
3957
|
-
const
|
|
3958
|
-
if (
|
|
4108
|
+
const guardedCount = manifest.entries.filter((e) => e.profile === "guarded" || e.private).length;
|
|
4109
|
+
if (guardedCount) {
|
|
3959
4110
|
disclosures.push(
|
|
3960
|
-
`machineProfile "personal": ${
|
|
4111
|
+
`machineProfile "personal": ${guardedCount} guarded entr${guardedCount === 1 ? "y" : "ies"} deliberately absent on this machine \u2014 disclosed, not dangling.`
|
|
3961
4112
|
);
|
|
3962
4113
|
}
|
|
3963
4114
|
}
|
|
3964
4115
|
return { findings, disclosures };
|
|
3965
4116
|
}
|
|
3966
|
-
async function
|
|
4117
|
+
async function checkGuardedWallArtifacts(entries, findings) {
|
|
3967
4118
|
for (const entry of entries) {
|
|
3968
|
-
if (entry.profile !== "
|
|
3969
|
-
for (const artifact of
|
|
4119
|
+
if (entry.profile !== "guarded" || !entry.resolvedRoot) continue;
|
|
4120
|
+
for (const artifact of GUARDED_WALL_ARTIFACTS) {
|
|
3970
4121
|
if (await pathExists(path2.join(entry.resolvedRoot, artifact))) {
|
|
3971
4122
|
findings.push(
|
|
3972
4123
|
finding3(
|
|
3973
|
-
`${FLEET_LENS}/
|
|
4124
|
+
`${FLEET_LENS}/guarded-artifact-in-repo:${entry.name}:${artifact}`,
|
|
3974
4125
|
"risk",
|
|
3975
|
-
`
|
|
4126
|
+
`guarded entry \`${entry.name}\` carries ${artifact} inside its worktree`,
|
|
3976
4127
|
[`${entry.resolvedRoot}/${artifact}`],
|
|
3977
|
-
"Agent artifacts for
|
|
3978
|
-
`Move it to the fleet's
|
|
4128
|
+
"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.",
|
|
4129
|
+
`Move it to the fleet's guarded/${entry.name}/ zone and leave a pointer if the harness needs one.`
|
|
3979
4130
|
)
|
|
3980
4131
|
);
|
|
3981
4132
|
}
|
|
@@ -3983,9 +4134,9 @@ async function checkCorpWallArtifacts(entries, findings) {
|
|
|
3983
4134
|
}
|
|
3984
4135
|
}
|
|
3985
4136
|
async function checkCoverage(manifest, findings, disclosures) {
|
|
3986
|
-
if (!manifest.
|
|
4137
|
+
if (!manifest.guardedHosts.length) {
|
|
3987
4138
|
disclosures.push(
|
|
3988
|
-
"Coverage check skipped \u2014 no
|
|
4139
|
+
"Coverage check skipped \u2014 no guardedHosts in the local manifest, so a guarded remote cannot be recognized."
|
|
3989
4140
|
);
|
|
3990
4141
|
return;
|
|
3991
4142
|
}
|
|
@@ -4006,16 +4157,16 @@ async function checkCoverage(manifest, findings, disclosures) {
|
|
|
4006
4157
|
if (!await pathExists(path2.join(dir, ".git"))) continue;
|
|
4007
4158
|
const remotes = await git(dir, ["config", "--get-regexp", "^remote\\..*\\.url"]);
|
|
4008
4159
|
if (!remotes) continue;
|
|
4009
|
-
const
|
|
4010
|
-
if (
|
|
4160
|
+
const guardedRemote = manifest.guardedHosts.find((h) => remotes.includes(h));
|
|
4161
|
+
if (guardedRemote) {
|
|
4011
4162
|
findings.push(
|
|
4012
4163
|
finding3(
|
|
4013
|
-
`${FLEET_LENS}/unregistered-
|
|
4164
|
+
`${FLEET_LENS}/unregistered-guarded-remote:${d.name}`,
|
|
4014
4165
|
"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
|
|
4166
|
+
`\`${d.name}\` under the fleet root has a guarded remote but is not registered`,
|
|
4167
|
+
[`${dir}: remote matches ${guardedRemote}`],
|
|
4168
|
+
"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.",
|
|
4169
|
+
"Register it as a guarded entry (opaque alias + local dirs mapping) or move it out of the fleet root."
|
|
4019
4170
|
)
|
|
4020
4171
|
);
|
|
4021
4172
|
}
|
|
@@ -4063,7 +4214,7 @@ async function checkHygieneNeedles(manifest, findings, disclosures) {
|
|
|
4063
4214
|
[
|
|
4064
4215
|
...Object.values(manifest.labels),
|
|
4065
4216
|
...Object.values(manifest.localDirs).map((d) => path2.basename(expandNeedleDir(d))),
|
|
4066
|
-
...manifest.
|
|
4217
|
+
...manifest.guardedHosts
|
|
4067
4218
|
].filter((n) => n.length >= 3)
|
|
4068
4219
|
)
|
|
4069
4220
|
];
|
|
@@ -4090,7 +4241,7 @@ async function checkHygieneNeedles(manifest, findings, disclosures) {
|
|
|
4090
4241
|
"risk",
|
|
4091
4242
|
`${file} in public-repo \`${entry.name}\` carries a needle from the fleet's private local manifest`,
|
|
4092
4243
|
[`${file}: contains "${needle}"`],
|
|
4093
|
-
"A public repo that names a private label,
|
|
4244
|
+
"A public repo that names a private label, guarded directory, or guarded host joins the public and guarded identities for anyone who greps.",
|
|
4094
4245
|
"Scrub the needle from the tracked file (rewrite history if it already shipped)."
|
|
4095
4246
|
)
|
|
4096
4247
|
);
|
|
@@ -4101,10 +4252,10 @@ async function checkHygieneNeedles(manifest, findings, disclosures) {
|
|
|
4101
4252
|
function expandNeedleDir(dir) {
|
|
4102
4253
|
return dir.replace(/\/+$/, "");
|
|
4103
4254
|
}
|
|
4104
|
-
async function
|
|
4105
|
-
if (!manifest.
|
|
4255
|
+
async function checkGuardedEmails(manifest, findings, disclosures) {
|
|
4256
|
+
if (!manifest.guardedHosts.length) {
|
|
4106
4257
|
disclosures.push(
|
|
4107
|
-
"
|
|
4258
|
+
"Guarded-email check skipped \u2014 no guardedHosts in the local manifest, so a guarded domain cannot be recognized."
|
|
4108
4259
|
);
|
|
4109
4260
|
return;
|
|
4110
4261
|
}
|
|
@@ -4113,21 +4264,23 @@ async function checkCorpEmails(manifest, findings, disclosures) {
|
|
|
4113
4264
|
const emails = await git(entry.resolvedRoot, ["log", "-30", "--format=%ae"]);
|
|
4114
4265
|
if (emails === null) {
|
|
4115
4266
|
disclosures.push(
|
|
4116
|
-
`
|
|
4267
|
+
`Guarded-email check could not run in \`${entry.name}\` (no readable commit history) \u2014 undetermined, not clean.`
|
|
4117
4268
|
);
|
|
4118
4269
|
continue;
|
|
4119
4270
|
}
|
|
4120
4271
|
const lines = emails.split("\n").filter(Boolean);
|
|
4121
|
-
const matched = [
|
|
4272
|
+
const matched = [
|
|
4273
|
+
...new Set(lines.filter((e) => emailMatchesGuardedHosts(e, manifest.guardedHosts)))
|
|
4274
|
+
];
|
|
4122
4275
|
if (matched.length) {
|
|
4123
4276
|
const count = lines.filter((e) => matched.includes(e)).length;
|
|
4124
4277
|
findings.push(
|
|
4125
4278
|
finding3(
|
|
4126
|
-
`${FLEET_LENS}/
|
|
4279
|
+
`${FLEET_LENS}/guarded-email:${entry.name}`,
|
|
4127
4280
|
"risk",
|
|
4128
|
-
`recent commits on personal entry \`${entry.name}\` are authored with a
|
|
4281
|
+
`recent commits on personal entry \`${entry.name}\` are authored with a guarded-host email`,
|
|
4129
4282
|
[`${count} of the last ${lines.length} commits: ${matched.join(", ")}`],
|
|
4130
|
-
"A
|
|
4283
|
+
"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
4284
|
"Set the repo-local identity to the personal one and amend the unpushed commits."
|
|
4132
4285
|
)
|
|
4133
4286
|
);
|
|
@@ -4141,7 +4294,7 @@ async function checkGateDrift(manifest, findings, disclosures) {
|
|
|
4141
4294
|
for (const entry of manifest.entries) {
|
|
4142
4295
|
const root = entry.resolvedRoot;
|
|
4143
4296
|
if (!root || !await isDirectory(root)) continue;
|
|
4144
|
-
if (entry.profile === "
|
|
4297
|
+
if (entry.profile === "guarded" || entry.private) continue;
|
|
4145
4298
|
if (entry.gates === "none") {
|
|
4146
4299
|
disclosures.push(`${entry.name}: gates declared \`none\` \u2014 deliberately absent, not missing`);
|
|
4147
4300
|
continue;
|
|
@@ -4194,11 +4347,11 @@ async function checkGateDrift(manifest, findings, disclosures) {
|
|
|
4194
4347
|
async function collectWallFindings(manifest) {
|
|
4195
4348
|
const findings = [];
|
|
4196
4349
|
const disclosures = [];
|
|
4197
|
-
await
|
|
4350
|
+
await checkGuardedWallArtifacts(manifest.entries, findings);
|
|
4198
4351
|
await checkCoverage(manifest, findings, disclosures);
|
|
4199
4352
|
await checkManifestRepoMachinePaths(manifest, findings, disclosures);
|
|
4200
4353
|
await checkHygieneNeedles(manifest, findings, disclosures);
|
|
4201
|
-
await
|
|
4354
|
+
await checkGuardedEmails(manifest, findings, disclosures);
|
|
4202
4355
|
await checkGateDrift(manifest, findings, disclosures);
|
|
4203
4356
|
return { findings, disclosures };
|
|
4204
4357
|
}
|
|
@@ -4264,13 +4417,13 @@ async function sweepEntry(manifest, entry, opts) {
|
|
|
4264
4417
|
);
|
|
4265
4418
|
}
|
|
4266
4419
|
}
|
|
4267
|
-
const
|
|
4268
|
-
const optedIn = !
|
|
4420
|
+
const guarded = entry.profile === "guarded";
|
|
4421
|
+
const optedIn = !guarded && await pathExists(path2.join(root, ETYMD_DIR));
|
|
4269
4422
|
const result = await runAudit(root, {
|
|
4270
4423
|
kind: opts.kind,
|
|
4271
|
-
persistLedger: !
|
|
4272
|
-
readOnlyRoot:
|
|
4273
|
-
ledgerRoot:
|
|
4424
|
+
persistLedger: !guarded && opts.persistLedgers === true && optedIn,
|
|
4425
|
+
readOnlyRoot: guarded,
|
|
4426
|
+
ledgerRoot: guarded ? guardedPersistenceRoot(manifest, entry.name) : void 0,
|
|
4274
4427
|
stateBudgets: stateBudgetsFor(entry),
|
|
4275
4428
|
upstreamRemote
|
|
4276
4429
|
});
|
|
@@ -4278,11 +4431,43 @@ async function sweepEntry(manifest, entry, opts) {
|
|
|
4278
4431
|
if (entry.contract.placement === "none") {
|
|
4279
4432
|
project.findings = project.findings.filter((f) => !f.id.endsWith("/no-contract"));
|
|
4280
4433
|
}
|
|
4434
|
+
const milestonesFile = entry.contract.milestones;
|
|
4435
|
+
if (milestonesFile && milestonesFile !== "none") {
|
|
4436
|
+
const text = await readText(path2.join(root, milestonesFile));
|
|
4437
|
+
if (text === null) {
|
|
4438
|
+
project.findings.push(
|
|
4439
|
+
finding3(
|
|
4440
|
+
`${FLEET_LENS}/milestones-missing:${entry.name}`,
|
|
4441
|
+
"gap",
|
|
4442
|
+
`\`${entry.name}\` declares milestones at \`${milestonesFile}\` and the file is absent`,
|
|
4443
|
+
[
|
|
4444
|
+
`${path2.basename(manifest.manifestPath)}: ${entry.name}.contract.milestones = ${milestonesFile}`
|
|
4445
|
+
],
|
|
4446
|
+
"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.",
|
|
4447
|
+
'Create it (heading `# Milestones`, then the standard table), or declare `milestones: "none"` if this project deliberately carries no plan.'
|
|
4448
|
+
)
|
|
4449
|
+
);
|
|
4450
|
+
} else {
|
|
4451
|
+
const doc = parseMilestones(text);
|
|
4452
|
+
doc.problems.forEach((problem, i) => {
|
|
4453
|
+
project.findings.push(
|
|
4454
|
+
finding3(
|
|
4455
|
+
`${FLEET_LENS}/milestones-shape:${entry.name}:${i + 1}`,
|
|
4456
|
+
"gap",
|
|
4457
|
+
`\`${entry.name}\` ${milestonesFile}: ${problem}`,
|
|
4458
|
+
[`${milestonesFile}`],
|
|
4459
|
+
"A milestones file the board cannot parse is a plan the fleet cannot see.",
|
|
4460
|
+
`Fix the table to the standard shape: \`| ${MILESTONE_COLUMNS.join(" | ")} |\`.`
|
|
4461
|
+
)
|
|
4462
|
+
);
|
|
4463
|
+
});
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4281
4466
|
for (const f of project.findings) project.counts[f.tier] += 1;
|
|
4282
4467
|
project.stateAgeDays = stateAgeDays(result.facts);
|
|
4283
4468
|
project.staleAfterDays = result.config.config.state.staleAfterDays;
|
|
4284
4469
|
for (const [key, value] of Object.entries(entry.contract)) {
|
|
4285
|
-
if (key === "placement" || !value) continue;
|
|
4470
|
+
if (key === "placement" || key === "milestones" || !value) continue;
|
|
4286
4471
|
const exists = await pathExists(path2.join(root, value.replace(/#.*$/, "")));
|
|
4287
4472
|
project.disclosures.push(
|
|
4288
4473
|
`contract override \`${key}: ${value}\` \u2014 registered, existence-checked only (${exists ? "present" : "MISSING"})`
|
|
@@ -4325,6 +4510,179 @@ async function sweepFleet(manifest, opts = {}) {
|
|
|
4325
4510
|
};
|
|
4326
4511
|
}
|
|
4327
4512
|
|
|
4513
|
+
// src/engine/propose.ts
|
|
4514
|
+
var PROPOSAL_SCHEMA = "proposal/1";
|
|
4515
|
+
var RUBRIC_CRITERIA = ["severity", "economy", "confidence", "breadth"];
|
|
4516
|
+
function parseRubric(text, file) {
|
|
4517
|
+
const lines = [];
|
|
4518
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4519
|
+
const rows = text.split(/\r?\n/);
|
|
4520
|
+
for (const [i, raw] of rows.entries()) {
|
|
4521
|
+
const line = raw.trim();
|
|
4522
|
+
if (!line || line.startsWith("#")) continue;
|
|
4523
|
+
const at = i + 1;
|
|
4524
|
+
const refuse = (why) => new Error(
|
|
4525
|
+
`${file}:${at} \`${raw.trim()}\` \u2014 ${why} (criteria: ${RUBRIC_CRITERIA.join(", ")})`
|
|
4526
|
+
);
|
|
4527
|
+
const sep = line.indexOf(":");
|
|
4528
|
+
if (sep < 0) throw refuse("expected `criterion: <weight>`");
|
|
4529
|
+
const criterion = line.slice(0, sep).trim();
|
|
4530
|
+
const weightRaw = line.slice(sep + 1).trim();
|
|
4531
|
+
if (!RUBRIC_CRITERIA.includes(criterion)) {
|
|
4532
|
+
throw refuse(`unknown criterion \`${criterion}\``);
|
|
4533
|
+
}
|
|
4534
|
+
if (!/^\d+$/.test(weightRaw) || Number(weightRaw) < 1) {
|
|
4535
|
+
throw refuse(`weight \`${weightRaw}\` is not a positive integer`);
|
|
4536
|
+
}
|
|
4537
|
+
const known = criterion;
|
|
4538
|
+
if (seen.has(known)) {
|
|
4539
|
+
throw new Error(
|
|
4540
|
+
`${file}:${at} \`${raw.trim()}\` \u2014 criterion \`${criterion}\` is already weighted on line ${seen.get(known)}; one line per criterion`
|
|
4541
|
+
);
|
|
4542
|
+
}
|
|
4543
|
+
seen.set(known, at);
|
|
4544
|
+
lines.push({ criterion: known, weight: Number(weightRaw) });
|
|
4545
|
+
}
|
|
4546
|
+
if (!lines.length) {
|
|
4547
|
+
throw new Error(`${file} carries no criterion lines \u2014 a rubric with no criteria scores nothing`);
|
|
4548
|
+
}
|
|
4549
|
+
return { lines };
|
|
4550
|
+
}
|
|
4551
|
+
var TIER_VALUE = { risk: 3, gap: 2, polish: 1 };
|
|
4552
|
+
var EFFORT_VALUE = { S: 3, M: 2, L: 1 };
|
|
4553
|
+
var CONFIDENCE_VALUE = { high: 3, medium: 2, low: 1 };
|
|
4554
|
+
function findingSubject(project, f) {
|
|
4555
|
+
return {
|
|
4556
|
+
id: `${project.name}:${f.id}`,
|
|
4557
|
+
kind: "finding",
|
|
4558
|
+
classId: f.id.split(":")[0] ?? f.id,
|
|
4559
|
+
tier: f.tier,
|
|
4560
|
+
effort: f.effort,
|
|
4561
|
+
confidence: f.confidence,
|
|
4562
|
+
projects: [project.name],
|
|
4563
|
+
action: f.action ?? "",
|
|
4564
|
+
evidence: f.evidence
|
|
4565
|
+
};
|
|
4566
|
+
}
|
|
4567
|
+
function classSubject(classId, members) {
|
|
4568
|
+
const pick = (order, at, dir) => {
|
|
4569
|
+
const values = members.map((m) => order[at(m.finding)]);
|
|
4570
|
+
const value = dir === 1 ? Math.max(...values) : Math.min(...values);
|
|
4571
|
+
return Object.entries(order).find(([, v]) => v === value)[0];
|
|
4572
|
+
};
|
|
4573
|
+
const actions = [...new Set(members.map((m) => m.finding.action).filter(Boolean))].sort();
|
|
4574
|
+
return {
|
|
4575
|
+
id: `class:${classId}`,
|
|
4576
|
+
kind: "class",
|
|
4577
|
+
classId,
|
|
4578
|
+
tier: pick(TIER_VALUE, (f) => f.tier, 1),
|
|
4579
|
+
effort: pick(EFFORT_VALUE, (f) => f.effort, -1),
|
|
4580
|
+
confidence: pick(CONFIDENCE_VALUE, (f) => f.confidence, -1),
|
|
4581
|
+
projects: [...new Set(members.map((m) => m.project.name))].sort(),
|
|
4582
|
+
action: actions.join("; "),
|
|
4583
|
+
evidence: members.flatMap((m) => m.finding.evidence)
|
|
4584
|
+
};
|
|
4585
|
+
}
|
|
4586
|
+
var CRITERION_VALUE = {
|
|
4587
|
+
severity: (s) => TIER_VALUE[s.tier],
|
|
4588
|
+
economy: (s) => EFFORT_VALUE[s.effort],
|
|
4589
|
+
confidence: (s) => CONFIDENCE_VALUE[s.confidence],
|
|
4590
|
+
breadth: (s) => Math.min(3, s.projects.length)
|
|
4591
|
+
};
|
|
4592
|
+
var FILENAME_RE = /^[^\s:]+\.[A-Za-z0-9]+$/;
|
|
4593
|
+
function extractFiles(evidence) {
|
|
4594
|
+
const files = /* @__PURE__ */ new Set();
|
|
4595
|
+
for (const line of evidence) {
|
|
4596
|
+
const token = line.split(/[\s:]/)[0];
|
|
4597
|
+
if (token && (token.includes("/") || FILENAME_RE.test(token))) files.add(token);
|
|
4598
|
+
}
|
|
4599
|
+
return [...files].sort();
|
|
4600
|
+
}
|
|
4601
|
+
function extractGates(files) {
|
|
4602
|
+
return files.filter((f) => f.startsWith(".githooks/") || f.startsWith(".github/workflows/"));
|
|
4603
|
+
}
|
|
4604
|
+
function reversibility(files) {
|
|
4605
|
+
if (!files.length) return "undetermined";
|
|
4606
|
+
const generated = (f) => f.startsWith(".etymd/") || f.startsWith(".githooks/");
|
|
4607
|
+
return files.every(generated) ? "regenerable" : "git-reversible";
|
|
4608
|
+
}
|
|
4609
|
+
function toProposal(s, rubric) {
|
|
4610
|
+
const matched = [];
|
|
4611
|
+
let score = 0;
|
|
4612
|
+
for (const line of rubric.lines) {
|
|
4613
|
+
const value = CRITERION_VALUE[line.criterion](s);
|
|
4614
|
+
score += line.weight * value;
|
|
4615
|
+
if (value >= 2) matched.push({ criterion: line.criterion, weight: line.weight, value });
|
|
4616
|
+
}
|
|
4617
|
+
const files = extractFiles(s.evidence);
|
|
4618
|
+
return {
|
|
4619
|
+
id: s.id,
|
|
4620
|
+
schema: PROPOSAL_SCHEMA,
|
|
4621
|
+
class: s.classId,
|
|
4622
|
+
kind: s.kind,
|
|
4623
|
+
projects: s.projects,
|
|
4624
|
+
action: s.action,
|
|
4625
|
+
effort: s.effort,
|
|
4626
|
+
confidence: s.confidence,
|
|
4627
|
+
score,
|
|
4628
|
+
matched,
|
|
4629
|
+
implications: {
|
|
4630
|
+
projects: s.projects,
|
|
4631
|
+
files,
|
|
4632
|
+
gates: extractGates(files),
|
|
4633
|
+
reversibility: reversibility(files)
|
|
4634
|
+
}
|
|
4635
|
+
};
|
|
4636
|
+
}
|
|
4637
|
+
function buildProposals(sweep, rubricName, rubric) {
|
|
4638
|
+
const disclosures = [];
|
|
4639
|
+
const personal = sweep.projects.filter((p) => p.profile === "personal");
|
|
4640
|
+
const guarded = sweep.projects.filter((p) => p.profile !== "personal");
|
|
4641
|
+
if (guarded.length) {
|
|
4642
|
+
disclosures.push(
|
|
4643
|
+
`guarded entries excluded from proposals: ${guarded.map((p) => p.name).sort().join(", ")}`
|
|
4644
|
+
);
|
|
4645
|
+
}
|
|
4646
|
+
const notAudited = personal.filter((p) => !p.resolvedRoot || p.unresolved);
|
|
4647
|
+
if (notAudited.length) {
|
|
4648
|
+
disclosures.push(
|
|
4649
|
+
`not audited, so not proposed: ${notAudited.map((p) => p.name).sort().join(", ")}`
|
|
4650
|
+
);
|
|
4651
|
+
}
|
|
4652
|
+
const subjects = [];
|
|
4653
|
+
for (const project of personal) {
|
|
4654
|
+
for (const f of project.findings) {
|
|
4655
|
+
if (f.kind === "improvement") subjects.push(findingSubject(project, f));
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4658
|
+
const byClass = /* @__PURE__ */ new Map();
|
|
4659
|
+
for (const project of personal) {
|
|
4660
|
+
for (const f of project.findings) {
|
|
4661
|
+
const classId = f.id.split(":")[0] ?? f.id;
|
|
4662
|
+
const entry = byClass.get(classId) ?? [];
|
|
4663
|
+
entry.push({ project, finding: f });
|
|
4664
|
+
byClass.set(classId, entry);
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
for (const [classId, members] of byClass) {
|
|
4668
|
+
if (new Set(members.map((m) => m.project.name)).size >= 2) {
|
|
4669
|
+
subjects.push(classSubject(classId, members));
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
const proposals = subjects.map((s) => toProposal(s, rubric)).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
4673
|
+
return {
|
|
4674
|
+
schema: PROPOSAL_SCHEMA,
|
|
4675
|
+
manifest: sweep.manifest,
|
|
4676
|
+
rubric: rubricName,
|
|
4677
|
+
criteria: rubric.lines,
|
|
4678
|
+
proposals,
|
|
4679
|
+
disclosures: [
|
|
4680
|
+
...disclosures,
|
|
4681
|
+
"implications.files are path-shaped tokens extracted from finding evidence \u2014 evidence lines carrying no path are not represented there"
|
|
4682
|
+
]
|
|
4683
|
+
};
|
|
4684
|
+
}
|
|
4685
|
+
|
|
4328
4686
|
// src/engine/premise.ts
|
|
4329
4687
|
init_facts();
|
|
4330
4688
|
init_scan();
|
|
@@ -4347,7 +4705,8 @@ function promoteBareTokens(text, ctx) {
|
|
|
4347
4705
|
bareInvocations: 0,
|
|
4348
4706
|
proseScripts: 0,
|
|
4349
4707
|
hostnameLike: 0,
|
|
4350
|
-
unrootedDirs: 0
|
|
4708
|
+
unrootedDirs: 0,
|
|
4709
|
+
namespaced: 0
|
|
4351
4710
|
};
|
|
4352
4711
|
const promoted = text.split(CODE_SPAN_RE).map((segment, i) => i % 2 === 1 ? segment : promoteProse(segment, ctx, skips)).join("");
|
|
4353
4712
|
return { text: promoted, skips };
|
|
@@ -4366,28 +4725,44 @@ function promoteProse(segment, ctx, skips) {
|
|
|
4366
4725
|
}
|
|
4367
4726
|
return `\`${core}\`${trail}`;
|
|
4368
4727
|
});
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4728
|
+
const parts = withCommands.split(/(\s+)/);
|
|
4729
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
4730
|
+
parts[i] = promoteToken(parts[i], i >= 2 ? parts[i - 2] : "", ctx, skips);
|
|
4731
|
+
}
|
|
4732
|
+
return parts.join("");
|
|
4733
|
+
}
|
|
4734
|
+
function namespaceOf(core, prev) {
|
|
4735
|
+
const ns = new RegExp(`^(${NAMESPACE_IDENT}):`).exec(core)?.[1] ?? new RegExp(`^(${NAMESPACE_IDENT}):$`).exec(prev)?.[1];
|
|
4736
|
+
return ns && !NAMESPACE_STOP.has(ns.toLowerCase()) ? ns : null;
|
|
4737
|
+
}
|
|
4738
|
+
function promoteToken(token, prev, ctx, skips) {
|
|
4739
|
+
const m = WRAP_RE.exec(token);
|
|
4740
|
+
if (!m) return token;
|
|
4741
|
+
const [, lead = "", core = "", trail = ""] = m;
|
|
4742
|
+
if (!core || core.includes("://")) return token;
|
|
4743
|
+
const namespace = namespaceOf(core, prev);
|
|
4744
|
+
const body = namespace && core.startsWith(`${namespace}:`) ? core.slice(namespace.length + 1) : core;
|
|
4745
|
+
if (!PATH_TOKEN_RE.test(body)) return token;
|
|
4746
|
+
const first = body.slice(0, body.indexOf("/"));
|
|
4747
|
+
const hostSuffix = HOSTNAME_RE.exec(first)?.[1]?.toLowerCase();
|
|
4748
|
+
if (hostSuffix && !KNOWN_EXTENSIONS.has(hostSuffix)) {
|
|
4749
|
+
skips.hostnameLike += 1;
|
|
4750
|
+
return token;
|
|
4751
|
+
}
|
|
4752
|
+
if (body.endsWith("/")) {
|
|
4753
|
+
if (!ctx.rootedDirs.has(first)) {
|
|
4754
|
+
skips.unrootedDirs += 1;
|
|
4378
4755
|
return token;
|
|
4379
4756
|
}
|
|
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];
|
|
4757
|
+
} else {
|
|
4758
|
+
const ext = body.toLowerCase().match(/\.([a-z0-9]{1,8})$/)?.[1];
|
|
4388
4759
|
if (!ext || !KNOWN_EXTENSIONS.has(ext)) return token;
|
|
4389
|
-
|
|
4390
|
-
|
|
4760
|
+
}
|
|
4761
|
+
if (namespace) {
|
|
4762
|
+
skips.namespaced += 1;
|
|
4763
|
+
return token;
|
|
4764
|
+
}
|
|
4765
|
+
return `${lead}\`${body}\`${trail}`;
|
|
4391
4766
|
}
|
|
4392
4767
|
async function listRootedDirs(env) {
|
|
4393
4768
|
const dirs = /* @__PURE__ */ new Set();
|
|
@@ -4405,7 +4780,8 @@ async function runPremise(opts) {
|
|
|
4405
4780
|
const facts = await scanProject(root);
|
|
4406
4781
|
const env = await buildTruthEnv(root, facts);
|
|
4407
4782
|
const counters = emptyCounters();
|
|
4408
|
-
const
|
|
4783
|
+
const rootedDirs = await listRootedDirs(env);
|
|
4784
|
+
const promoted = promoteBareTokens(task, { rootedDirs });
|
|
4409
4785
|
const text = { path: TASK_LABEL, text: promoted.text };
|
|
4410
4786
|
const findings = [];
|
|
4411
4787
|
const disclosures = [];
|
|
@@ -4418,6 +4794,8 @@ async function runPremise(opts) {
|
|
|
4418
4794
|
subject: "The task",
|
|
4419
4795
|
missingPathTier: "risk",
|
|
4420
4796
|
maxPathFindings: MAX_PATH_FINDINGS,
|
|
4797
|
+
rootedFirstSegments: rootedDirs,
|
|
4798
|
+
treatNamespacedPrefixes: true,
|
|
4421
4799
|
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
4800
|
actionCommand: "Fix the task before handing it over: name the real script, or restore it.",
|
|
4423
4801
|
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 +4852,21 @@ async function runPremise(opts) {
|
|
|
4474
4852
|
`${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
4853
|
);
|
|
4476
4854
|
}
|
|
4855
|
+
if (skips.namespaced) {
|
|
4856
|
+
disclosures.push(
|
|
4857
|
+
`${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.`
|
|
4858
|
+
);
|
|
4859
|
+
}
|
|
4860
|
+
if (counters.namespacedSkipped) {
|
|
4861
|
+
disclosures.push(
|
|
4862
|
+
`${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.`
|
|
4863
|
+
);
|
|
4864
|
+
}
|
|
4865
|
+
if (counters.outsideRepoSkipped) {
|
|
4866
|
+
disclosures.push(
|
|
4867
|
+
`${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.`
|
|
4868
|
+
);
|
|
4869
|
+
}
|
|
4477
4870
|
if (counters.unverifiableCommands) {
|
|
4478
4871
|
disclosures.push(
|
|
4479
4872
|
`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 +4987,4 @@ wrong problem.
|
|
|
4594
4987
|
init_version();
|
|
4595
4988
|
init_version2();
|
|
4596
4989
|
|
|
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,
|
|
4990
|
+
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 };
|