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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { applyFiles } from './chunk-GWGKEPRX.js';
|
|
3
|
-
import { planWorkflow } from './chunk-
|
|
3
|
+
import { planWorkflow } from './chunk-OKJ3RGJB.js';
|
|
4
4
|
import { theme, renderFacts, print, renderPlan, section, glyph } from './chunk-HI7NWPRA.js';
|
|
5
|
-
import { scanProject } from './chunk-
|
|
6
|
-
import { VERSION } from './chunk-
|
|
5
|
+
import { scanProject } from './chunk-EJM47PIF.js';
|
|
6
|
+
import { VERSION } from './chunk-UYCGP5BS.js';
|
|
7
7
|
import { writeCachedFacts, deriveProfile, writeBaseline, CACHE_DIR } from './chunk-P6ATKV2R.js';
|
|
8
8
|
import { PACK_VERSION } from './chunk-YQZDYDAK.js';
|
|
9
9
|
import { git, readText } from './chunk-4VPBP6K6.js';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { parseFailOnTier, meetsFailOn, buildTruthEnv, emptyCounters, checkTextClaims, checkDocRefs, loadDecisionLedger, checkDecisionRefs, rankFindings, PATH_TOKEN_RE, KNOWN_EXTENSIONS } from './chunk-
|
|
2
|
+
import { parseFailOnTier, meetsFailOn, buildTruthEnv, emptyCounters, checkTextClaims, checkDocRefs, loadDecisionLedger, checkDecisionRefs, rankFindings, PATH_TOKEN_RE, KNOWN_EXTENSIONS, NAMESPACE_IDENT, NAMESPACE_STOP } from './chunk-Y4E6ERCM.js';
|
|
3
3
|
import { print, section, theme, renderFindings } from './chunk-HI7NWPRA.js';
|
|
4
|
-
import { scanProject } from './chunk-
|
|
5
|
-
import './chunk-
|
|
4
|
+
import { scanProject } from './chunk-EJM47PIF.js';
|
|
5
|
+
import './chunk-UYCGP5BS.js';
|
|
6
6
|
import { ETYMD_DIR } from './chunk-P6ATKV2R.js';
|
|
7
7
|
import './chunk-YQZDYDAK.js';
|
|
8
8
|
import { readText, pathExists } from './chunk-4VPBP6K6.js';
|
|
@@ -27,7 +27,8 @@ function promoteBareTokens(text, ctx) {
|
|
|
27
27
|
bareInvocations: 0,
|
|
28
28
|
proseScripts: 0,
|
|
29
29
|
hostnameLike: 0,
|
|
30
|
-
unrootedDirs: 0
|
|
30
|
+
unrootedDirs: 0,
|
|
31
|
+
namespaced: 0
|
|
31
32
|
};
|
|
32
33
|
const promoted = text.split(CODE_SPAN_RE).map((segment, i) => i % 2 === 1 ? segment : promoteProse(segment, ctx, skips)).join("");
|
|
33
34
|
return { text: promoted, skips };
|
|
@@ -46,28 +47,44 @@ function promoteProse(segment, ctx, skips) {
|
|
|
46
47
|
}
|
|
47
48
|
return `\`${core}\`${trail}`;
|
|
48
49
|
});
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
const parts = withCommands.split(/(\s+)/);
|
|
51
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
52
|
+
parts[i] = promoteToken(parts[i], i >= 2 ? parts[i - 2] : "", ctx, skips);
|
|
53
|
+
}
|
|
54
|
+
return parts.join("");
|
|
55
|
+
}
|
|
56
|
+
function namespaceOf(core, prev) {
|
|
57
|
+
const ns = new RegExp(`^(${NAMESPACE_IDENT}):`).exec(core)?.[1] ?? new RegExp(`^(${NAMESPACE_IDENT}):$`).exec(prev)?.[1];
|
|
58
|
+
return ns && !NAMESPACE_STOP.has(ns.toLowerCase()) ? ns : null;
|
|
59
|
+
}
|
|
60
|
+
function promoteToken(token, prev, ctx, skips) {
|
|
61
|
+
const m = WRAP_RE.exec(token);
|
|
62
|
+
if (!m) return token;
|
|
63
|
+
const [, lead = "", core = "", trail = ""] = m;
|
|
64
|
+
if (!core || core.includes("://")) return token;
|
|
65
|
+
const namespace = namespaceOf(core, prev);
|
|
66
|
+
const body = namespace && core.startsWith(`${namespace}:`) ? core.slice(namespace.length + 1) : core;
|
|
67
|
+
if (!PATH_TOKEN_RE.test(body)) return token;
|
|
68
|
+
const first = body.slice(0, body.indexOf("/"));
|
|
69
|
+
const hostSuffix = HOSTNAME_RE.exec(first)?.[1]?.toLowerCase();
|
|
70
|
+
if (hostSuffix && !KNOWN_EXTENSIONS.has(hostSuffix)) {
|
|
71
|
+
skips.hostnameLike += 1;
|
|
72
|
+
return token;
|
|
73
|
+
}
|
|
74
|
+
if (body.endsWith("/")) {
|
|
75
|
+
if (!ctx.rootedDirs.has(first)) {
|
|
76
|
+
skips.unrootedDirs += 1;
|
|
58
77
|
return token;
|
|
59
78
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
skips.unrootedDirs += 1;
|
|
63
|
-
return token;
|
|
64
|
-
}
|
|
65
|
-
return `${lead}\`${core}\`${trail}`;
|
|
66
|
-
}
|
|
67
|
-
const ext = core.toLowerCase().match(/\.([a-z0-9]{1,8})$/)?.[1];
|
|
79
|
+
} else {
|
|
80
|
+
const ext = body.toLowerCase().match(/\.([a-z0-9]{1,8})$/)?.[1];
|
|
68
81
|
if (!ext || !KNOWN_EXTENSIONS.has(ext)) return token;
|
|
69
|
-
|
|
70
|
-
|
|
82
|
+
}
|
|
83
|
+
if (namespace) {
|
|
84
|
+
skips.namespaced += 1;
|
|
85
|
+
return token;
|
|
86
|
+
}
|
|
87
|
+
return `${lead}\`${body}\`${trail}`;
|
|
71
88
|
}
|
|
72
89
|
async function listRootedDirs(env) {
|
|
73
90
|
const dirs = /* @__PURE__ */ new Set();
|
|
@@ -85,7 +102,8 @@ async function runPremise(opts) {
|
|
|
85
102
|
const facts = await scanProject(root);
|
|
86
103
|
const env = await buildTruthEnv(root, facts);
|
|
87
104
|
const counters = emptyCounters();
|
|
88
|
-
const
|
|
105
|
+
const rootedDirs = await listRootedDirs(env);
|
|
106
|
+
const promoted = promoteBareTokens(task, { rootedDirs });
|
|
89
107
|
const text = { path: TASK_LABEL, text: promoted.text };
|
|
90
108
|
const findings = [];
|
|
91
109
|
const disclosures = [];
|
|
@@ -98,6 +116,8 @@ async function runPremise(opts) {
|
|
|
98
116
|
subject: "The task",
|
|
99
117
|
missingPathTier: "risk",
|
|
100
118
|
maxPathFindings: MAX_PATH_FINDINGS,
|
|
119
|
+
rootedFirstSegments: rootedDirs,
|
|
120
|
+
treatNamespacedPrefixes: true,
|
|
101
121
|
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.",
|
|
102
122
|
actionCommand: "Fix the task before handing it over: name the real script, or restore it.",
|
|
103
123
|
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.",
|
|
@@ -154,6 +174,21 @@ async function runPremise(opts) {
|
|
|
154
174
|
`${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.`
|
|
155
175
|
);
|
|
156
176
|
}
|
|
177
|
+
if (skips.namespaced) {
|
|
178
|
+
disclosures.push(
|
|
179
|
+
`${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.`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (counters.namespacedSkipped) {
|
|
183
|
+
disclosures.push(
|
|
184
|
+
`${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.`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (counters.outsideRepoSkipped) {
|
|
188
|
+
disclosures.push(
|
|
189
|
+
`${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.`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
157
192
|
if (counters.unverifiableCommands) {
|
|
158
193
|
disclosures.push(
|
|
159
194
|
`node_modules is not installed \u2014 ${counters.unverifiableCommands} command(s) matching no package script could not be checked against installed binaries; skipped, not flagged.`
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { sweepFleet, loadFleetManifest, FLEET_JSON_SCHEMA } from './chunk-ANXGT7V3.js';
|
|
3
|
+
import './chunk-UV3YIBDW.js';
|
|
4
|
+
import './chunk-Y4E6ERCM.js';
|
|
5
|
+
import './chunk-3E2IPCRY.js';
|
|
6
|
+
import { print, section, theme, renderFleetNotes } from './chunk-HI7NWPRA.js';
|
|
7
|
+
import './chunk-EJM47PIF.js';
|
|
8
|
+
import './chunk-UYCGP5BS.js';
|
|
9
|
+
import './chunk-P6ATKV2R.js';
|
|
10
|
+
import './chunk-YQZDYDAK.js';
|
|
11
|
+
import { readText } from './chunk-4VPBP6K6.js';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
|
|
14
|
+
// src/engine/propose.ts
|
|
15
|
+
var PROPOSAL_SCHEMA = "proposal/1";
|
|
16
|
+
var RUBRIC_CRITERIA = ["severity", "economy", "confidence", "breadth"];
|
|
17
|
+
function parseRubric(text, file) {
|
|
18
|
+
const lines = [];
|
|
19
|
+
const seen = /* @__PURE__ */ new Map();
|
|
20
|
+
const rows = text.split(/\r?\n/);
|
|
21
|
+
for (const [i, raw] of rows.entries()) {
|
|
22
|
+
const line = raw.trim();
|
|
23
|
+
if (!line || line.startsWith("#")) continue;
|
|
24
|
+
const at = i + 1;
|
|
25
|
+
const refuse = (why) => new Error(
|
|
26
|
+
`${file}:${at} \`${raw.trim()}\` \u2014 ${why} (criteria: ${RUBRIC_CRITERIA.join(", ")})`
|
|
27
|
+
);
|
|
28
|
+
const sep = line.indexOf(":");
|
|
29
|
+
if (sep < 0) throw refuse("expected `criterion: <weight>`");
|
|
30
|
+
const criterion = line.slice(0, sep).trim();
|
|
31
|
+
const weightRaw = line.slice(sep + 1).trim();
|
|
32
|
+
if (!RUBRIC_CRITERIA.includes(criterion)) {
|
|
33
|
+
throw refuse(`unknown criterion \`${criterion}\``);
|
|
34
|
+
}
|
|
35
|
+
if (!/^\d+$/.test(weightRaw) || Number(weightRaw) < 1) {
|
|
36
|
+
throw refuse(`weight \`${weightRaw}\` is not a positive integer`);
|
|
37
|
+
}
|
|
38
|
+
const known = criterion;
|
|
39
|
+
if (seen.has(known)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`${file}:${at} \`${raw.trim()}\` \u2014 criterion \`${criterion}\` is already weighted on line ${seen.get(known)}; one line per criterion`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
seen.set(known, at);
|
|
45
|
+
lines.push({ criterion: known, weight: Number(weightRaw) });
|
|
46
|
+
}
|
|
47
|
+
if (!lines.length) {
|
|
48
|
+
throw new Error(`${file} carries no criterion lines \u2014 a rubric with no criteria scores nothing`);
|
|
49
|
+
}
|
|
50
|
+
return { lines };
|
|
51
|
+
}
|
|
52
|
+
var TIER_VALUE = { risk: 3, gap: 2, polish: 1 };
|
|
53
|
+
var EFFORT_VALUE = { S: 3, M: 2, L: 1 };
|
|
54
|
+
var CONFIDENCE_VALUE = { high: 3, medium: 2, low: 1 };
|
|
55
|
+
function findingSubject(project, f) {
|
|
56
|
+
return {
|
|
57
|
+
id: `${project.name}:${f.id}`,
|
|
58
|
+
kind: "finding",
|
|
59
|
+
classId: f.id.split(":")[0] ?? f.id,
|
|
60
|
+
tier: f.tier,
|
|
61
|
+
effort: f.effort,
|
|
62
|
+
confidence: f.confidence,
|
|
63
|
+
projects: [project.name],
|
|
64
|
+
action: f.action ?? "",
|
|
65
|
+
evidence: f.evidence
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function classSubject(classId, members) {
|
|
69
|
+
const pick = (order, at, dir) => {
|
|
70
|
+
const values = members.map((m) => order[at(m.finding)]);
|
|
71
|
+
const value = dir === 1 ? Math.max(...values) : Math.min(...values);
|
|
72
|
+
return Object.entries(order).find(([, v]) => v === value)[0];
|
|
73
|
+
};
|
|
74
|
+
const actions = [...new Set(members.map((m) => m.finding.action).filter(Boolean))].sort();
|
|
75
|
+
return {
|
|
76
|
+
id: `class:${classId}`,
|
|
77
|
+
kind: "class",
|
|
78
|
+
classId,
|
|
79
|
+
tier: pick(TIER_VALUE, (f) => f.tier, 1),
|
|
80
|
+
effort: pick(EFFORT_VALUE, (f) => f.effort, -1),
|
|
81
|
+
confidence: pick(CONFIDENCE_VALUE, (f) => f.confidence, -1),
|
|
82
|
+
projects: [...new Set(members.map((m) => m.project.name))].sort(),
|
|
83
|
+
action: actions.join("; "),
|
|
84
|
+
evidence: members.flatMap((m) => m.finding.evidence)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
var CRITERION_VALUE = {
|
|
88
|
+
severity: (s) => TIER_VALUE[s.tier],
|
|
89
|
+
economy: (s) => EFFORT_VALUE[s.effort],
|
|
90
|
+
confidence: (s) => CONFIDENCE_VALUE[s.confidence],
|
|
91
|
+
breadth: (s) => Math.min(3, s.projects.length)
|
|
92
|
+
};
|
|
93
|
+
var FILENAME_RE = /^[^\s:]+\.[A-Za-z0-9]+$/;
|
|
94
|
+
function extractFiles(evidence) {
|
|
95
|
+
const files = /* @__PURE__ */ new Set();
|
|
96
|
+
for (const line of evidence) {
|
|
97
|
+
const token = line.split(/[\s:]/)[0];
|
|
98
|
+
if (token && (token.includes("/") || FILENAME_RE.test(token))) files.add(token);
|
|
99
|
+
}
|
|
100
|
+
return [...files].sort();
|
|
101
|
+
}
|
|
102
|
+
function extractGates(files) {
|
|
103
|
+
return files.filter((f) => f.startsWith(".githooks/") || f.startsWith(".github/workflows/"));
|
|
104
|
+
}
|
|
105
|
+
function reversibility(files) {
|
|
106
|
+
if (!files.length) return "undetermined";
|
|
107
|
+
const generated = (f) => f.startsWith(".etymd/") || f.startsWith(".githooks/");
|
|
108
|
+
return files.every(generated) ? "regenerable" : "git-reversible";
|
|
109
|
+
}
|
|
110
|
+
function toProposal(s, rubric) {
|
|
111
|
+
const matched = [];
|
|
112
|
+
let score = 0;
|
|
113
|
+
for (const line of rubric.lines) {
|
|
114
|
+
const value = CRITERION_VALUE[line.criterion](s);
|
|
115
|
+
score += line.weight * value;
|
|
116
|
+
if (value >= 2) matched.push({ criterion: line.criterion, weight: line.weight, value });
|
|
117
|
+
}
|
|
118
|
+
const files = extractFiles(s.evidence);
|
|
119
|
+
return {
|
|
120
|
+
id: s.id,
|
|
121
|
+
schema: PROPOSAL_SCHEMA,
|
|
122
|
+
class: s.classId,
|
|
123
|
+
kind: s.kind,
|
|
124
|
+
projects: s.projects,
|
|
125
|
+
action: s.action,
|
|
126
|
+
effort: s.effort,
|
|
127
|
+
confidence: s.confidence,
|
|
128
|
+
score,
|
|
129
|
+
matched,
|
|
130
|
+
implications: {
|
|
131
|
+
projects: s.projects,
|
|
132
|
+
files,
|
|
133
|
+
gates: extractGates(files),
|
|
134
|
+
reversibility: reversibility(files)
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function buildProposals(sweep, rubricName, rubric) {
|
|
139
|
+
const disclosures = [];
|
|
140
|
+
const personal = sweep.projects.filter((p) => p.profile === "personal");
|
|
141
|
+
const guarded = sweep.projects.filter((p) => p.profile !== "personal");
|
|
142
|
+
if (guarded.length) {
|
|
143
|
+
disclosures.push(
|
|
144
|
+
`guarded entries excluded from proposals: ${guarded.map((p) => p.name).sort().join(", ")}`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const notAudited = personal.filter((p) => !p.resolvedRoot || p.unresolved);
|
|
148
|
+
if (notAudited.length) {
|
|
149
|
+
disclosures.push(
|
|
150
|
+
`not audited, so not proposed: ${notAudited.map((p) => p.name).sort().join(", ")}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const subjects = [];
|
|
154
|
+
for (const project of personal) {
|
|
155
|
+
for (const f of project.findings) {
|
|
156
|
+
if (f.kind === "improvement") subjects.push(findingSubject(project, f));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const byClass = /* @__PURE__ */ new Map();
|
|
160
|
+
for (const project of personal) {
|
|
161
|
+
for (const f of project.findings) {
|
|
162
|
+
const classId = f.id.split(":")[0] ?? f.id;
|
|
163
|
+
const entry = byClass.get(classId) ?? [];
|
|
164
|
+
entry.push({ project, finding: f });
|
|
165
|
+
byClass.set(classId, entry);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const [classId, members] of byClass) {
|
|
169
|
+
if (new Set(members.map((m) => m.project.name)).size >= 2) {
|
|
170
|
+
subjects.push(classSubject(classId, members));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const proposals = subjects.map((s) => toProposal(s, rubric)).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
174
|
+
return {
|
|
175
|
+
schema: PROPOSAL_SCHEMA,
|
|
176
|
+
manifest: sweep.manifest,
|
|
177
|
+
rubric: rubricName,
|
|
178
|
+
criteria: rubric.lines,
|
|
179
|
+
proposals,
|
|
180
|
+
disclosures: [
|
|
181
|
+
...disclosures,
|
|
182
|
+
"implications.files are path-shaped tokens extracted from finding evidence \u2014 evidence lines carrying no path are not represented there"
|
|
183
|
+
]
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/commands/propose.ts
|
|
188
|
+
async function readStoredSweep(abs) {
|
|
189
|
+
const raw = await readText(abs);
|
|
190
|
+
if (raw === null) throw new Error(`cannot read the stored sweep: ${abs}`);
|
|
191
|
+
let parsed;
|
|
192
|
+
try {
|
|
193
|
+
parsed = JSON.parse(raw);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`${abs} is not valid JSON (${err instanceof Error ? err.message : String(err)})`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const rec = parsed;
|
|
200
|
+
if (typeof rec !== "object" || rec === null || rec.schema !== FLEET_JSON_SCHEMA || !Array.isArray(rec.projects)) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`${abs} is not a stored fleet sweep (schema ${FLEET_JSON_SCHEMA}) \u2014 write one with \`etymd fleet --json\``
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return { manifest: rec.manifest ?? abs, projects: rec.projects };
|
|
206
|
+
}
|
|
207
|
+
async function run(opts) {
|
|
208
|
+
if (!opts.manifest && !opts.from) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
"propose needs a sweep: pass --manifest <registry.json> (runs one now, read-only) or --from <fleet.json> (reads a stored `etymd fleet --json` output)"
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (opts.manifest && opts.from) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
"--manifest and --from are alternatives \u2014 a fresh sweep or a stored one, not both"
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const rubricPath = path.resolve(opts.cwd, opts.rubric);
|
|
219
|
+
const rubricText = await readText(rubricPath);
|
|
220
|
+
if (rubricText === null) throw new Error(`cannot read the rubric: ${rubricPath}`);
|
|
221
|
+
const rubric = parseRubric(rubricText, opts.rubric);
|
|
222
|
+
const swept = opts.from ? await readStoredSweep(path.resolve(opts.cwd, opts.from)) : await sweepFleet(await loadFleetManifest(path.resolve(opts.cwd, opts.manifest)));
|
|
223
|
+
const result = buildProposals(swept, opts.rubric, rubric);
|
|
224
|
+
if (opts.json) {
|
|
225
|
+
print(JSON.stringify(result, null, 2));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
render(result);
|
|
229
|
+
}
|
|
230
|
+
function render(result) {
|
|
231
|
+
section(
|
|
232
|
+
`Proposals ${theme.dim(`\xB7 ${path.basename(result.manifest)} \xB7 rubric ${result.rubric} (${result.criteria.map((l) => `${l.criterion}:${l.weight}`).join(", ")}) \xB7 ${result.proposals.length} proposal(s) \xB7 schema ${result.schema} (EXPERIMENTAL)`)}`
|
|
233
|
+
);
|
|
234
|
+
if (!result.proposals.length) {
|
|
235
|
+
print(` ${theme.dim("nothing to propose \u2014 no improvement findings or recurring classes")}`);
|
|
236
|
+
}
|
|
237
|
+
for (const p of result.proposals) {
|
|
238
|
+
const who = p.kind === "class" ? `${p.class} ${theme.dim(`\u2014 class, ${p.projects.length}\xD7: ${p.projects.join(", ")}`)}` : `${p.id} ${theme.dim(`\u2014 ${p.projects.join(", ")}`)}`;
|
|
239
|
+
print(` ${theme.info(String(p.score).padStart(3))} ${who}`);
|
|
240
|
+
print(
|
|
241
|
+
` ${theme.dim(`${p.effort} \xB7 ${p.confidence} \xB7 fired: ${p.matched.map((m) => `${m.criterion}(${m.value}/3)`).join(", ") || "\u2014"}`)}`
|
|
242
|
+
);
|
|
243
|
+
if (p.action) print(` ${theme.dim(p.action)}`);
|
|
244
|
+
const im = p.implications;
|
|
245
|
+
print(
|
|
246
|
+
` ${theme.dim(`implications: ${im.projects.length} project(s) \xB7 files: ${im.files.join(", ") || "\u2014"} \xB7 gates: ${im.gates.join(", ") || "\u2014"} \xB7 ${im.reversibility}`)}`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
renderFleetNotes([], result.disclosures);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export { run };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { print, renderFacts, theme } from './chunk-HI7NWPRA.js';
|
|
3
|
-
import { scanProject } from './chunk-
|
|
4
|
-
import './chunk-
|
|
3
|
+
import { scanProject } from './chunk-EJM47PIF.js';
|
|
4
|
+
import './chunk-UYCGP5BS.js';
|
|
5
5
|
import { writeCachedFacts } from './chunk-P6ATKV2R.js';
|
|
6
6
|
import './chunk-YQZDYDAK.js';
|
|
7
7
|
import './chunk-4VPBP6K6.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "etymd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Keep your agent instructions true — 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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
"author": "Alkis Yuv",
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
|
-
"url": "git+https://github.com/
|
|
22
|
+
"url": "git+https://github.com/fleetorders/etymd.git"
|
|
23
23
|
},
|
|
24
|
-
"homepage": "https://github.com/
|
|
24
|
+
"homepage": "https://github.com/fleetorders/etymd#readme",
|
|
25
25
|
"bugs": {
|
|
26
|
-
"url": "https://github.com/
|
|
26
|
+
"url": "https://github.com/fleetorders/etymd/issues"
|
|
27
27
|
},
|
|
28
28
|
"type": "module",
|
|
29
29
|
"bin": {
|