etymd 0.13.0 → 0.15.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 +108 -0
- package/README.md +30 -19
- package/dist/{approve-YUT43YLC.js → approve-MA4Z3TBT.js} +5 -5
- package/dist/audit-YSALDC2L.js +11 -0
- package/dist/{brief-Z5S6OY2M.js → brief-DPZYSAMC.js} +5 -5
- package/dist/{chunk-PXRLOEN5.js → chunk-2RNQ6OLV.js} +146 -717
- package/dist/{chunk-DSAQ5S5D.js → chunk-CEO3BXQB.js} +3 -2
- package/dist/{chunk-D3R74TJ2.js → chunk-DBWDMIYO.js} +1 -1
- package/dist/{chunk-5BVKFJWM.js → chunk-HI7NWPRA.js} +79 -4
- package/dist/{chunk-2VLNI3L2.js → chunk-HOR4M6EC.js} +1 -1
- package/dist/chunk-IWG77WV3.js +758 -0
- package/dist/{chunk-DWL2IKZH.js → chunk-P6ATKV2R.js} +65 -3
- package/dist/{chunk-HRJJQCMT.js → chunk-UFNETE6P.js} +69 -20
- package/dist/{chunk-YXOAPMQH.js → chunk-Y6RZRED3.js} +2 -2
- package/dist/{chunk-LSZGCKIQ.js → chunk-YQZDYDAK.js} +1 -1
- package/dist/cli.js +42 -21
- package/dist/{config-XAH6PA5G.js → config-724Y3IOB.js} +1 -2
- package/dist/{context-F63RSIBH.js → context-JGKU4M7Z.js} +2 -4
- package/dist/doctor-Y3DWDEBT.js +18 -0
- package/dist/{fleet-YQ35KGEP.js → fleet-4FYZ3GBK.js} +11 -12
- package/dist/{gates-DU6SDH2M.js → gates-IBQ7HCAN.js} +6 -7
- package/dist/generate-KEX75XNG.js +5 -0
- package/dist/index.d.ts +173 -2
- package/dist/index.js +713 -198
- package/dist/{init-XJEYU2HC.js → init-H57H2JBE.js} +14 -13
- package/dist/ledger-T54JDHGP.js +6 -0
- package/dist/premise-VMC3UUIB.js +333 -0
- package/dist/scan-W4US23MU.js +5 -0
- package/dist/{scan-B24RZAV2.js → scan-XBOHDXAH.js} +5 -5
- package/dist/{screen-CS6PSG7U.js → screen-E4FC7W5N.js} +2 -1
- package/package.json +2 -2
- package/dist/audit-YRT4SWSQ.js +0 -12
- package/dist/chunk-F75Q43BC.js +0 -59
- package/dist/chunk-JHZ2BN4U.js +0 -67
- package/dist/doctor-7YKDXSKM.js +0 -19
- package/dist/generate-PMCP37DC.js +0 -6
- package/dist/ledger-4FOZ5HAB.js +0 -5
- package/dist/scan-QDNN7ER3.js +0 -5
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { applyFiles } from './chunk-GWGKEPRX.js';
|
|
3
|
-
import { planWorkflow } from './chunk-
|
|
4
|
-
import './chunk-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { PACK_VERSION } from './chunk-LSZGCKIQ.js';
|
|
3
|
+
import { planWorkflow } from './chunk-UFNETE6P.js';
|
|
4
|
+
import { theme, renderFacts, print, renderPlan, section, glyph } from './chunk-HI7NWPRA.js';
|
|
5
|
+
import { scanProject } from './chunk-Y6RZRED3.js';
|
|
6
|
+
import { VERSION } from './chunk-DBWDMIYO.js';
|
|
7
|
+
import { writeCachedFacts, deriveProfile, writeBaseline, CACHE_DIR } from './chunk-P6ATKV2R.js';
|
|
8
|
+
import { PACK_VERSION } from './chunk-YQZDYDAK.js';
|
|
10
9
|
import { git, readText } from './chunk-4VPBP6K6.js';
|
|
11
10
|
import { promises } from 'fs';
|
|
12
11
|
import path from 'path';
|
|
@@ -57,12 +56,14 @@ async function run(opts) {
|
|
|
57
56
|
const hasContract = facts.artifacts.some((a) => a.id === "agents" && a.exists);
|
|
58
57
|
let scaffoldAgents = false;
|
|
59
58
|
if (!hasContract) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
if (opts.withAgents) scaffoldAgents = true;
|
|
60
|
+
else if (!opts.yes)
|
|
61
|
+
scaffoldAgents = guard(
|
|
62
|
+
await confirm({
|
|
63
|
+
message: "No AGENTS.md found \u2014 scaffold a minimal one from the reckoning?",
|
|
64
|
+
initialValue: true
|
|
65
|
+
})
|
|
66
|
+
);
|
|
66
67
|
}
|
|
67
68
|
let gates = false;
|
|
68
69
|
if (facts.git.isRepo && facts.hooks.source === "none") {
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseFailOnTier, meetsFailOn, buildTruthEnv, emptyCounters, checkTextClaims, checkDocRefs, loadDecisionLedger, checkDecisionRefs, rankFindings, PATH_TOKEN_RE, KNOWN_EXTENSIONS } from './chunk-IWG77WV3.js';
|
|
3
|
+
import { print, section, theme, renderFindings } from './chunk-HI7NWPRA.js';
|
|
4
|
+
import { scanProject } from './chunk-Y6RZRED3.js';
|
|
5
|
+
import './chunk-DBWDMIYO.js';
|
|
6
|
+
import { ETYMD_DIR } from './chunk-P6ATKV2R.js';
|
|
7
|
+
import './chunk-YQZDYDAK.js';
|
|
8
|
+
import { readText, pathExists } from './chunk-4VPBP6K6.js';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { promises } from 'fs';
|
|
11
|
+
|
|
12
|
+
var PREMISE_LENS = "premise";
|
|
13
|
+
var PREMISE_BRIEF_FILE = path.join(ETYMD_DIR, "premise-brief.md");
|
|
14
|
+
var TASK_LABEL = "task";
|
|
15
|
+
var MAX_PATH_FINDINGS = 15;
|
|
16
|
+
var CODE_SPAN_RE = /(```[\s\S]*?```|`[^`\n]+`)/g;
|
|
17
|
+
var RUN_RE = /\b(?:pnpm|yarn|npm|bun)\s+run\s+(?!-)([A-Za-z0-9:._-]+)|\bnpm\s+(?:test|start)\b/g;
|
|
18
|
+
var BARE_PM_RE = /\b(?:pnpm|yarn|bun)\s+(?!run\b)(?!-)[A-Za-z0-9:._-]+/g;
|
|
19
|
+
var STOP_WORDS = new Set(
|
|
20
|
+
"a an the this that these those it its them all any both each every some your our my their and or but to in on at of for with from by as is are was were be will would can could should may might must do does did not no so if then than when where which who what how also again now first once twice after before via into over only just too very".split(" ")
|
|
21
|
+
);
|
|
22
|
+
var WRAP_RE = /^([([{"']*)(.*?)([.,;:!?)\]}"']*)$/;
|
|
23
|
+
var TRAIL_RE = /[.,;:!?)\]}"']*$/;
|
|
24
|
+
var HOSTNAME_RE = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*\.([a-z]{2,})$/i;
|
|
25
|
+
function promoteBareTokens(text, ctx) {
|
|
26
|
+
const skips = {
|
|
27
|
+
bareInvocations: 0,
|
|
28
|
+
proseScripts: 0,
|
|
29
|
+
hostnameLike: 0,
|
|
30
|
+
unrootedDirs: 0
|
|
31
|
+
};
|
|
32
|
+
const promoted = text.split(CODE_SPAN_RE).map((segment, i) => i % 2 === 1 ? segment : promoteProse(segment, ctx, skips)).join("");
|
|
33
|
+
return { text: promoted, skips };
|
|
34
|
+
}
|
|
35
|
+
function promoteProse(segment, ctx, skips) {
|
|
36
|
+
skips.bareInvocations += segment.match(BARE_PM_RE)?.length ?? 0;
|
|
37
|
+
const withCommands = segment.replace(RUN_RE, (m, script) => {
|
|
38
|
+
const trail = TRAIL_RE.exec(m)?.[0] ?? "";
|
|
39
|
+
const core = trail ? m.slice(0, -trail.length) : m;
|
|
40
|
+
if (script !== void 0) {
|
|
41
|
+
const name = script.replace(TRAIL_RE, "");
|
|
42
|
+
if (name.length < 2 || STOP_WORDS.has(name.toLowerCase())) {
|
|
43
|
+
skips.proseScripts += 1;
|
|
44
|
+
return m;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return `\`${core}\`${trail}`;
|
|
48
|
+
});
|
|
49
|
+
return withCommands.replace(/[^\s`]+/g, (token) => {
|
|
50
|
+
const m = WRAP_RE.exec(token);
|
|
51
|
+
if (!m) return token;
|
|
52
|
+
const [, lead = "", core = "", trail = ""] = m;
|
|
53
|
+
if (!core || core.includes("://") || !PATH_TOKEN_RE.test(core)) return token;
|
|
54
|
+
const first = core.slice(0, core.indexOf("/"));
|
|
55
|
+
const hostSuffix = HOSTNAME_RE.exec(first)?.[1]?.toLowerCase();
|
|
56
|
+
if (hostSuffix && !KNOWN_EXTENSIONS.has(hostSuffix)) {
|
|
57
|
+
skips.hostnameLike += 1;
|
|
58
|
+
return token;
|
|
59
|
+
}
|
|
60
|
+
if (core.endsWith("/")) {
|
|
61
|
+
if (!ctx.rootedDirs.has(first)) {
|
|
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];
|
|
68
|
+
if (!ext || !KNOWN_EXTENSIONS.has(ext)) return token;
|
|
69
|
+
return `${lead}\`${core}\`${trail}`;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async function listRootedDirs(env) {
|
|
73
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
74
|
+
for (const base of env.bases) {
|
|
75
|
+
for (const sub of ["", "src", "scripts"]) {
|
|
76
|
+
const entries = await promises.readdir(path.join(base, sub), { withFileTypes: true }).catch(() => []);
|
|
77
|
+
for (const entry of entries) if (entry.isDirectory()) dirs.add(entry.name);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return dirs;
|
|
81
|
+
}
|
|
82
|
+
async function runPremise(opts) {
|
|
83
|
+
const root = opts.root;
|
|
84
|
+
const task = opts.task.trim();
|
|
85
|
+
const facts = await scanProject(root);
|
|
86
|
+
const env = await buildTruthEnv(root, facts);
|
|
87
|
+
const counters = emptyCounters();
|
|
88
|
+
const promoted = promoteBareTokens(task, { rootedDirs: await listRootedDirs(env) });
|
|
89
|
+
const text = { path: TASK_LABEL, text: promoted.text };
|
|
90
|
+
const findings = [];
|
|
91
|
+
const disclosures = [];
|
|
92
|
+
const entities = [];
|
|
93
|
+
const claims = await checkTextClaims(
|
|
94
|
+
env,
|
|
95
|
+
text,
|
|
96
|
+
{
|
|
97
|
+
lensId: PREMISE_LENS,
|
|
98
|
+
subject: "The task",
|
|
99
|
+
missingPathTier: "risk",
|
|
100
|
+
maxPathFindings: MAX_PATH_FINDINGS,
|
|
101
|
+
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
|
+
actionCommand: "Fix the task before handing it over: name the real script, or restore it.",
|
|
103
|
+
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.",
|
|
104
|
+
actionPath: "Find where the behaviour actually lives and rewrite the task to name it."
|
|
105
|
+
},
|
|
106
|
+
counters
|
|
107
|
+
);
|
|
108
|
+
findings.push(...claims.findings);
|
|
109
|
+
disclosures.push(...claims.disclosures);
|
|
110
|
+
entities.push(...claims.examined);
|
|
111
|
+
const docs = await checkDocRefs(env, text, PREMISE_LENS, counters, "The task");
|
|
112
|
+
findings.push(...docs.findings);
|
|
113
|
+
entities.push(...docs.examined);
|
|
114
|
+
const ledger = await loadDecisionLedger(root, facts);
|
|
115
|
+
const decisions = checkDecisionRefs(
|
|
116
|
+
text,
|
|
117
|
+
ledger,
|
|
118
|
+
{
|
|
119
|
+
lensId: PREMISE_LENS,
|
|
120
|
+
subject: "The task",
|
|
121
|
+
why: "The task presupposes a ruling that was never recorded; an agent will act on a decision nobody made.",
|
|
122
|
+
action: "Point the task at the entry that exists \u2014 or record the missing decision first."
|
|
123
|
+
},
|
|
124
|
+
counters
|
|
125
|
+
);
|
|
126
|
+
findings.push(...decisions.findings);
|
|
127
|
+
entities.push(...decisions.examined);
|
|
128
|
+
if (!entities.length) {
|
|
129
|
+
disclosures.push(
|
|
130
|
+
"The task names no path with a directory, no package script, no well-known doc, and no decision id \u2014 nothing etymd can check by reading the repo. Everything it presupposes is in the brief."
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
disclosures.push(
|
|
134
|
+
"In prose, a path needs a directory and a recognized extension (a directory claim needs a trailing slash and a first segment that exists here), and a script needs the `run` form (`npm run X`, `pnpm run X` \u2026) or the `npm test` / `npm start` shorthands; anything backticked is read exactly as an instruction file would be. A bare file name (a lone `config.ts`) is prose."
|
|
135
|
+
);
|
|
136
|
+
const { skips } = promoted;
|
|
137
|
+
if (skips.bareInvocations) {
|
|
138
|
+
disclosures.push(
|
|
139
|
+
`${skips.bareInvocations} bare \`pnpm X\` / \`yarn X\` / \`bun X\` mention(s) in prose were not read as scripts \u2014 in a sentence that shape is a phrase as often as an invocation; write \`\u2026 run X\` or backtick it to have it checked.`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (skips.proseScripts) {
|
|
143
|
+
disclosures.push(
|
|
144
|
+
`${skips.proseScripts} \`\u2026 run X\` mention(s) put a function word or a one-letter stand-in where a script name would be; read as prose, skipped.`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (skips.hostnameLike) {
|
|
148
|
+
disclosures.push(
|
|
149
|
+
`${skips.hostnameLike} slash token(s) start with a host name (e.g. \`github.com/\u2026\`) \u2014 a URL, not a repo path; skipped.`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (skips.unrootedDirs) {
|
|
153
|
+
disclosures.push(
|
|
154
|
+
`${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
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (counters.unverifiableCommands) {
|
|
158
|
+
disclosures.push(
|
|
159
|
+
`node_modules is not installed \u2014 ${counters.unverifiableCommands} command(s) matching no package script could not be checked against installed binaries; skipped, not flagged.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (counters.binaryResolved) {
|
|
163
|
+
disclosures.push(
|
|
164
|
+
`${counters.binaryResolved} command(s) resolve to installed binaries (node_modules/.bin), not package scripts \u2014 treated as true.`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (counters.gitignoredSkipped) {
|
|
168
|
+
disclosures.push(
|
|
169
|
+
`${counters.gitignoredSkipped} missing path(s) are gitignored (machine-local) \u2014 existence is not verifiable from the repo; skipped, not flagged.`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (counters.prospectiveSkipped) {
|
|
173
|
+
disclosures.push(
|
|
174
|
+
`${counters.prospectiveSkipped} path(s) sit in create-this prose (the task says to create them) \u2014 forward-looking, not missing; skipped.`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (counters.placeholderSkipped) {
|
|
178
|
+
disclosures.push(
|
|
179
|
+
`${counters.placeholderSkipped} path(s) are naming stand-ins (e.g. \`my-feature\`) rather than real references; skipped.`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (counters.filteredSkipped) {
|
|
183
|
+
disclosures.push(
|
|
184
|
+
`${counters.filteredSkipped} workspace-filtered command(s) (\`--filter\`, \`-C\` \u2026) were not resolved; skipped.`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (counters.tildeSkipped) {
|
|
188
|
+
disclosures.push(
|
|
189
|
+
`${counters.tildeSkipped} well-known doc mention(s) sit inside \`~/\` home paths (e.g. \`~/.claude/CLAUDE.md\`) \u2014 machine-global files, not this repo's; skipped, not flagged.`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
if (counters.qualifiedRefsSkipped) {
|
|
193
|
+
disclosures.push(
|
|
194
|
+
`${counters.qualifiedRefsSkipped} decision reference(s) name another record \u2014 not this repo's; skipped.`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (counters.unresolvableRefs) {
|
|
198
|
+
disclosures.push(
|
|
199
|
+
`${counters.unresolvableRefs} decision reference(s) could not be resolved \u2014 no decisions file with \`## D-NNN\` entries; skipped, not flagged.`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
disclosures.push(
|
|
203
|
+
"What the task PRESUPPOSES \u2014 that the named things are the ones meant, that the mechanism it assumes actually runs, that the state it assumes holds \u2014 cannot be checked by reading files. The brief hands those to the agent."
|
|
204
|
+
);
|
|
205
|
+
disclosures.push("Task findings are not remembered between runs \u2014 no ledger is written.");
|
|
206
|
+
const ranked = rankFindings(findings);
|
|
207
|
+
const brief = renderBrief(task, ranked, entities);
|
|
208
|
+
let briefPath = null;
|
|
209
|
+
if (opts.writeBrief !== false && await pathExists(path.join(root, ETYMD_DIR))) {
|
|
210
|
+
await promises.writeFile(path.join(root, PREMISE_BRIEF_FILE), brief, "utf8");
|
|
211
|
+
briefPath = PREMISE_BRIEF_FILE;
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
schema: "premise/1",
|
|
215
|
+
task,
|
|
216
|
+
source: opts.source ?? "argument",
|
|
217
|
+
name: facts.name,
|
|
218
|
+
findings: ranked,
|
|
219
|
+
disclosures,
|
|
220
|
+
entities,
|
|
221
|
+
brief,
|
|
222
|
+
briefPath
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function mark(exists) {
|
|
226
|
+
return exists === null ? "?" : exists ? "\u2713" : "\u2717";
|
|
227
|
+
}
|
|
228
|
+
function kindLabel(kind) {
|
|
229
|
+
return kind === "script" ? "script " : kind === "decision" ? "decision " : "";
|
|
230
|
+
}
|
|
231
|
+
function renderBrief(task, findings, entities) {
|
|
232
|
+
const oneLine = task.replace(/\s+/g, " ").trim();
|
|
233
|
+
const title = oneLine.length > 100 ? `${oneLine.slice(0, 97)}\u2026` : oneLine;
|
|
234
|
+
const checked = entities.length ? entities.map(
|
|
235
|
+
(e) => `- ${mark(e.exists)} ${kindLabel(e.kind)}\`${e.value}\`${e.exists === null ? " \u2014 could not be checked" : e.exists ? " \u2014 exists" : " \u2014 missing (see the finding)"}`
|
|
236
|
+
).join("\n") : "- (nothing the task names could be checked by reading the repo)";
|
|
237
|
+
const failed = findings.filter((f) => f.tier === "risk");
|
|
238
|
+
const failedLine = failed.length ? `
|
|
239
|
+
**${failed.length} premise(s) already failed** \u2014 the task names things that do not exist. Resolve those before anything below.
|
|
240
|
+
` : "";
|
|
241
|
+
return `# Premise brief \u2014 ${title}
|
|
242
|
+
|
|
243
|
+
The task, verbatim:
|
|
244
|
+
|
|
245
|
+
> ${task.replace(/\n/g, "\n> ")}
|
|
246
|
+
|
|
247
|
+
etymd checked what the task names against the repo. What it could NOT check is listed under
|
|
248
|
+
"only you can verify"; check those before acting, and if one fails, say so first \u2014 everything
|
|
249
|
+
else is downstream of it.
|
|
250
|
+
${failedLine}
|
|
251
|
+
## What the task names, and whether it exists
|
|
252
|
+
|
|
253
|
+
${checked}
|
|
254
|
+
|
|
255
|
+
## Premises only you can verify
|
|
256
|
+
|
|
257
|
+
1. **The named things are the ones MEANT.** A file that exists can still be the wrong file, and a
|
|
258
|
+
script that exists can still not be the one the task has in mind. Confirm each is where the
|
|
259
|
+
behaviour actually lives before touching it.
|
|
260
|
+
2. **The mechanism the task assumes is working.** A task shaped like "make X faster / less flaky /
|
|
261
|
+
fresh after Y" assumes X and Y already do what it says. Run them once. If the assumed
|
|
262
|
+
mechanism does not run, the task is downstream of a defect it does not mention.
|
|
263
|
+
3. **The state the task assumes holds.** Read it; do not trust the task's description of it.
|
|
264
|
+
|
|
265
|
+
## If a premise fails
|
|
266
|
+
|
|
267
|
+
Report it first, in one line: what the task presupposes, and what you found instead. Do not
|
|
268
|
+
answer the original task as if the premise held \u2014 a wrong problem solved precisely is still the
|
|
269
|
+
wrong problem.
|
|
270
|
+
`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/commands/premise.ts
|
|
274
|
+
async function readStdin() {
|
|
275
|
+
const chunks = [];
|
|
276
|
+
for await (const chunk of process.stdin) {
|
|
277
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
278
|
+
}
|
|
279
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
280
|
+
}
|
|
281
|
+
async function run(opts) {
|
|
282
|
+
const failOn = opts.failOn === void 0 ? void 0 : parseFailOnTier(opts.failOn);
|
|
283
|
+
let task = opts.task;
|
|
284
|
+
let source = "argument";
|
|
285
|
+
if (opts.file === "-") {
|
|
286
|
+
task = await readStdin();
|
|
287
|
+
source = "stdin";
|
|
288
|
+
} else if (opts.file) {
|
|
289
|
+
const abs = path.resolve(opts.cwd, opts.file);
|
|
290
|
+
const text = await readText(abs);
|
|
291
|
+
if (text === null) throw new Error(`could not read the task file: ${opts.file}`);
|
|
292
|
+
task = text;
|
|
293
|
+
source = opts.file;
|
|
294
|
+
}
|
|
295
|
+
if (!task?.trim()) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
'give the task as an argument (etymd premise "\u2026"), with --file <path>, or on stdin (--file -)'
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const result = await runPremise({
|
|
301
|
+
root: opts.cwd,
|
|
302
|
+
task,
|
|
303
|
+
source,
|
|
304
|
+
writeBrief: opts.brief !== false
|
|
305
|
+
});
|
|
306
|
+
if (failOn && meetsFailOn(result.findings, failOn)) {
|
|
307
|
+
process.exitCode = 1;
|
|
308
|
+
}
|
|
309
|
+
if (opts.json) {
|
|
310
|
+
print(JSON.stringify(result, null, 2));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
section(`Premise ${theme.dim(`\xB7 ${result.name} \xB7 is this the right task?`)}`);
|
|
314
|
+
const shown = result.task.length > 160 ? `${result.task.slice(0, 157)}\u2026` : result.task;
|
|
315
|
+
print(` ${theme.dim("task")} ${shown.replace(/\n/g, " ")}`);
|
|
316
|
+
section(`Findings ${theme.dim("(a missing thing the task is about ranks as risk)")}`);
|
|
317
|
+
renderFindings(result.findings);
|
|
318
|
+
for (const d of result.disclosures) print(` ${theme.dim(`\u25E6 ${d}`)}`);
|
|
319
|
+
if (opts.brief === false) return;
|
|
320
|
+
section("Brief");
|
|
321
|
+
if (result.briefPath) {
|
|
322
|
+
print(` ${theme.dim("wrote")} ${theme.info(result.briefPath)}`);
|
|
323
|
+
print(
|
|
324
|
+
` ${theme.dim("Hand it to the agent with the task:")} ${theme.dim('"Read')} ${theme.info(result.briefPath)}${theme.dim(' before you start."')}`
|
|
325
|
+
);
|
|
326
|
+
} else {
|
|
327
|
+
print(` ${theme.dim("(no .etymd/ here, so nothing is written \u2014 the brief follows)")}`);
|
|
328
|
+
print();
|
|
329
|
+
for (const line of result.brief.split("\n")) print(` ${line}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export { run };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { print, renderFacts, theme } from './chunk-
|
|
3
|
-
import { scanProject } from './chunk-
|
|
4
|
-
import './chunk-
|
|
5
|
-
import { writeCachedFacts } from './chunk-
|
|
6
|
-
import './chunk-
|
|
2
|
+
import { print, renderFacts, theme } from './chunk-HI7NWPRA.js';
|
|
3
|
+
import { scanProject } from './chunk-Y6RZRED3.js';
|
|
4
|
+
import './chunk-DBWDMIYO.js';
|
|
5
|
+
import { writeCachedFacts } from './chunk-P6ATKV2R.js';
|
|
6
|
+
import './chunk-YQZDYDAK.js';
|
|
7
7
|
import './chunk-4VPBP6K6.js';
|
|
8
8
|
|
|
9
9
|
// src/commands/scan.ts
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { print, glyph, theme, section } from './chunk-
|
|
2
|
+
import { print, glyph, theme, section } from './chunk-HI7NWPRA.js';
|
|
3
|
+
import './chunk-P6ATKV2R.js';
|
|
3
4
|
import { git, readText, pathExists } from './chunk-4VPBP6K6.js';
|
|
4
5
|
import { promises } from 'fs';
|
|
5
6
|
import path from 'path';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "etymd",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Keep your agent instructions true — verify AGENTS.md, CLAUDE.md, rules
|
|
3
|
+
"version": "0.15.0",
|
|
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",
|
|
7
7
|
"ai",
|
package/dist/audit-YRT4SWSQ.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
export { run } from './chunk-DSAQ5S5D.js';
|
|
3
|
-
import './chunk-PXRLOEN5.js';
|
|
4
|
-
import './chunk-F75Q43BC.js';
|
|
5
|
-
import './chunk-3E2IPCRY.js';
|
|
6
|
-
import './chunk-DWL2IKZH.js';
|
|
7
|
-
import './chunk-5BVKFJWM.js';
|
|
8
|
-
import './chunk-YXOAPMQH.js';
|
|
9
|
-
import './chunk-D3R74TJ2.js';
|
|
10
|
-
import './chunk-JHZ2BN4U.js';
|
|
11
|
-
import './chunk-LSZGCKIQ.js';
|
|
12
|
-
import './chunk-4VPBP6K6.js';
|
package/dist/chunk-F75Q43BC.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { DEFAULT_CONFIG } from './chunk-DWL2IKZH.js';
|
|
3
|
-
import { readText, wordCount, approxTokens, pathExists } from './chunk-4VPBP6K6.js';
|
|
4
|
-
import { promises } from 'fs';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
|
|
7
|
-
var ALWAYS_LOADED = [
|
|
8
|
-
{ path: "AGENTS.md", role: "operating contract" },
|
|
9
|
-
{ path: "PROJECT_CONTEXT.md", role: "ground-truth state" },
|
|
10
|
-
{ path: "CLAUDE.md", role: "Claude Code pointer" },
|
|
11
|
-
{ path: "GEMINI.md", role: "Gemini pointer" },
|
|
12
|
-
{ path: ".github/copilot-instructions.md", role: "Copilot instructions" },
|
|
13
|
-
{ path: ".cursorrules", role: "Cursor rules (legacy)" }
|
|
14
|
-
];
|
|
15
|
-
var EXTRACTION_THRESHOLD = DEFAULT_CONFIG.context.perFileWords;
|
|
16
|
-
function isAlwaysAppliedCursorRule(text) {
|
|
17
|
-
const fm = text.match(/^---\n([\s\S]*?)\n---/);
|
|
18
|
-
if (!fm) return true;
|
|
19
|
-
const frontmatter = fm[1] ?? "";
|
|
20
|
-
return /^\s*alwaysApply\s*:\s*true\s*$/m.test(frontmatter);
|
|
21
|
-
}
|
|
22
|
-
async function measureContext(root, perFileWords = EXTRACTION_THRESHOLD) {
|
|
23
|
-
const files = [];
|
|
24
|
-
for (const spec of ALWAYS_LOADED) {
|
|
25
|
-
const abs = path.join(root, spec.path);
|
|
26
|
-
const text = await readText(abs);
|
|
27
|
-
if (text === null) continue;
|
|
28
|
-
const words = wordCount(text);
|
|
29
|
-
files.push({ path: spec.path, role: spec.role, words, approxTokens: approxTokens(words) });
|
|
30
|
-
}
|
|
31
|
-
const rulesDir = path.join(root, ".cursor", "rules");
|
|
32
|
-
if (await pathExists(rulesDir)) {
|
|
33
|
-
try {
|
|
34
|
-
for (const entry of await promises.readdir(rulesDir)) {
|
|
35
|
-
if (!entry.endsWith(".mdc") && !entry.endsWith(".md")) continue;
|
|
36
|
-
const text = await readText(path.join(rulesDir, entry));
|
|
37
|
-
if (text === null || !isAlwaysAppliedCursorRule(text)) continue;
|
|
38
|
-
const words = wordCount(text);
|
|
39
|
-
files.push({
|
|
40
|
-
path: `.cursor/rules/${entry}`,
|
|
41
|
-
role: "Cursor rule (always applied)",
|
|
42
|
-
words,
|
|
43
|
-
approxTokens: approxTokens(words)
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
} catch {
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
const totalWords = files.reduce((s, f) => s + f.words, 0);
|
|
50
|
-
return {
|
|
51
|
-
files: files.sort((a, b) => b.words - a.words),
|
|
52
|
-
totalWords,
|
|
53
|
-
totalApproxTokens: approxTokens(totalWords),
|
|
54
|
-
perFileWords,
|
|
55
|
-
extractionCandidates: files.filter((f) => f.words >= perFileWords)
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export { measureContext };
|
package/dist/chunk-JHZ2BN4U.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { readJson } from './chunk-4VPBP6K6.js';
|
|
3
|
-
import { promises } from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
|
|
6
|
-
var ETYMD_DIR = ".etymd";
|
|
7
|
-
var CACHE_DIR = path.join(ETYMD_DIR, "cache");
|
|
8
|
-
var CACHE_FACTS_FILE = path.join(CACHE_DIR, "facts.json");
|
|
9
|
-
var BASELINE_FILE = path.join(ETYMD_DIR, "baseline.json");
|
|
10
|
-
function cacheFactsPath(root) {
|
|
11
|
-
return path.join(root, CACHE_FACTS_FILE);
|
|
12
|
-
}
|
|
13
|
-
function baselinePath(root) {
|
|
14
|
-
return path.join(root, BASELINE_FILE);
|
|
15
|
-
}
|
|
16
|
-
async function writeJsonFile(target, data) {
|
|
17
|
-
await promises.mkdir(path.dirname(target), { recursive: true });
|
|
18
|
-
await promises.writeFile(target, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
19
|
-
return target;
|
|
20
|
-
}
|
|
21
|
-
async function writeCachedFacts(root, facts) {
|
|
22
|
-
return writeJsonFile(cacheFactsPath(root), facts);
|
|
23
|
-
}
|
|
24
|
-
function withoutMachinePath(facts) {
|
|
25
|
-
return { ...facts, root: "." };
|
|
26
|
-
}
|
|
27
|
-
function baselineCarriesMachinePath(baseline) {
|
|
28
|
-
const root = baseline.facts?.root;
|
|
29
|
-
return typeof root === "string" && root !== "." && (root.startsWith("/") || /^[A-Za-z]:[\\/]/.test(root));
|
|
30
|
-
}
|
|
31
|
-
async function writeBaseline(root, baseline) {
|
|
32
|
-
return writeJsonFile(baselinePath(root), {
|
|
33
|
-
...baseline,
|
|
34
|
-
facts: withoutMachinePath(baseline.facts)
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
async function readBaseline(root) {
|
|
38
|
-
return readJson(baselinePath(root));
|
|
39
|
-
}
|
|
40
|
-
function deriveProfile(facts) {
|
|
41
|
-
return (facts.git.recentAuthors ?? 1) > 2 ? "team" : "solo";
|
|
42
|
-
}
|
|
43
|
-
var DRIFT_ROLES = ["test", "lint", "typecheck", "format", "formatCheck", "build", "dev"];
|
|
44
|
-
function summarizeBaselineDrift(old, fresh) {
|
|
45
|
-
const commands = [];
|
|
46
|
-
for (const role of DRIFT_ROLES) {
|
|
47
|
-
const from = old.commands[role];
|
|
48
|
-
const to = fresh.commands[role];
|
|
49
|
-
if (from !== to) commands.push({ role, from, to });
|
|
50
|
-
}
|
|
51
|
-
const presentIds = (f) => new Set(f.artifacts.filter((a) => a.exists).map((a) => a.id));
|
|
52
|
-
const oldArtifacts = presentIds(old);
|
|
53
|
-
const freshArtifacts = presentIds(fresh);
|
|
54
|
-
const labelOf = (id) => fresh.artifacts.find((a) => a.id === id)?.label ?? old.artifacts.find((a) => a.id === id)?.label ?? id;
|
|
55
|
-
const artifactsAdded = [...freshArtifacts].filter((id) => !oldArtifacts.has(id)).map(labelOf);
|
|
56
|
-
const artifactsRemoved = [...oldArtifacts].filter((id) => !freshArtifacts.has(id)).map(labelOf);
|
|
57
|
-
const oldDirs = new Set(old.tree.dirs.map((d) => d.name));
|
|
58
|
-
const freshDirs = new Set(fresh.tree.dirs.map((d) => d.name));
|
|
59
|
-
const dirsAdded = [...freshDirs].filter((d) => !oldDirs.has(d));
|
|
60
|
-
const dirsRemoved = [...oldDirs].filter((d) => !freshDirs.has(d));
|
|
61
|
-
return { commands, artifactsAdded, artifactsRemoved, dirsAdded, dirsRemoved };
|
|
62
|
-
}
|
|
63
|
-
function isDriftEmpty(d) {
|
|
64
|
-
return d.commands.length === 0 && d.artifactsAdded.length === 0 && d.artifactsRemoved.length === 0 && d.dirsAdded.length === 0 && d.dirsRemoved.length === 0;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export { BASELINE_FILE, CACHE_DIR, ETYMD_DIR, baselineCarriesMachinePath, deriveProfile, isDriftEmpty, readBaseline, summarizeBaselineDrift, writeBaseline, writeCachedFacts };
|
package/dist/doctor-7YKDXSKM.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { run } from './chunk-DSAQ5S5D.js';
|
|
3
|
-
import './chunk-PXRLOEN5.js';
|
|
4
|
-
import './chunk-F75Q43BC.js';
|
|
5
|
-
import './chunk-3E2IPCRY.js';
|
|
6
|
-
import './chunk-DWL2IKZH.js';
|
|
7
|
-
import './chunk-5BVKFJWM.js';
|
|
8
|
-
import './chunk-YXOAPMQH.js';
|
|
9
|
-
import './chunk-D3R74TJ2.js';
|
|
10
|
-
import './chunk-JHZ2BN4U.js';
|
|
11
|
-
import './chunk-LSZGCKIQ.js';
|
|
12
|
-
import './chunk-4VPBP6K6.js';
|
|
13
|
-
|
|
14
|
-
// src/commands/doctor.ts
|
|
15
|
-
async function run2(opts) {
|
|
16
|
-
await run({ cwd: opts.cwd, json: opts.json, truth: true });
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export { run2 as run };
|
package/dist/ledger-4FOZ5HAB.js
DELETED