@intentic/sandbox-contract 1.223.0 → 1.225.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/README.md +14 -14
- package/dist/agent-catalog.js +3 -3
- package/dist/agent-catalog.js.map +1 -1
- package/dist/chores/chores.js +43 -43
- package/dist/chores/chores.js.map +1 -1
- package/dist/chores/extension-update.js +2 -2
- package/dist/chores/extension-update.js.map +1 -1
- package/dist/chores/fix-deps.js +1 -1
- package/dist/chores/fix-deps.js.map +1 -1
- package/dist/chores/probes.js +1 -1
- package/dist/chores/probes.js.map +1 -1
- package/dist/chores/prompt.d.ts.map +1 -1
- package/dist/chores/prompt.js +3 -3
- package/dist/chores/prompt.js.map +1 -1
- package/dist/chores/verdict.js +2 -2
- package/dist/chores/verdict.js.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +41 -0
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/exit.contract.d.ts +80 -0
- package/dist/contracts/exit.contract.d.ts.map +1 -0
- package/dist/contracts/exit.contract.js +13 -0
- package/dist/contracts/exit.contract.js.map +1 -0
- package/dist/contracts/ports.contract.d.ts +11 -0
- package/dist/contracts/ports.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +12 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/events.d.ts +4 -4
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +5 -5
- package/dist/events.js.map +1 -1
- package/dist/fast-tier.d.ts +9 -0
- package/dist/fast-tier.d.ts.map +1 -0
- package/dist/fast-tier.js +19 -0
- package/dist/fast-tier.js.map +1 -0
- package/dist/history-state.js +3 -3
- package/dist/history-state.js.map +1 -1
- package/dist/index.d.ts +214 -68
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/model-order.d.ts +1 -0
- package/dist/model-order.d.ts.map +1 -1
- package/dist/model-order.js +5 -0
- package/dist/model-order.js.map +1 -1
- package/dist/output-fields.d.ts.map +1 -1
- package/dist/output-fields.js +2 -2
- package/dist/output-fields.js.map +1 -1
- package/dist/prompt-complexity.d.ts +19 -0
- package/dist/prompt-complexity.d.ts.map +1 -0
- package/dist/prompt-complexity.js +83 -0
- package/dist/prompt-complexity.js.map +1 -0
- package/dist/publish-drafts.js +2 -2
- package/dist/publish-drafts.js.map +1 -1
- package/dist/schemas.d.ts +236 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +116 -2
- package/dist/schemas.js.map +1 -1
- package/dist/workflow-faults.js +3 -3
- package/dist/workflow-faults.js.map +1 -1
- package/dist/workspace-state.d.ts +20 -20
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +20 -20
- package/dist/workspace-state.js.map +1 -1
- package/package.json +5 -5
- package/src/agent-catalog.test.ts +25 -25
- package/src/agent-catalog.ts +81 -81
- package/src/agent-run-model.test.ts +3 -3
- package/src/agent-run-model.ts +8 -8
- package/src/capability-env.ts +3 -3
- package/src/capability-ledger.test.ts +13 -13
- package/src/capability-secrets.ts +5 -5
- package/src/chores/chores.test.ts +1 -1
- package/src/chores/chores.ts +109 -109
- package/src/chores/digest.test.ts +1 -1
- package/src/chores/digest.ts +3 -3
- package/src/chores/extension-update.ts +5 -5
- package/src/chores/fix-deps.ts +4 -4
- package/src/chores/probes.test.ts +6 -6
- package/src/chores/probes.ts +17 -17
- package/src/chores/prompt.ts +9 -9
- package/src/chores/stack.test.ts +3 -3
- package/src/chores/stack.ts +17 -17
- package/src/chores/verdict.test.ts +20 -20
- package/src/chores/verdict.ts +25 -25
- package/src/contract-lock.test.ts +1 -1
- package/src/contract-lock.ts +7 -7
- package/src/contracts/activity.contract.ts +1 -1
- package/src/contracts/agent.contract.ts +2 -2
- package/src/contracts/agents.contract.ts +11 -11
- package/src/contracts/automations.contract.ts +5 -5
- package/src/contracts/capabilities.contract.ts +7 -7
- package/src/contracts/chores.contract.ts +3 -3
- package/src/contracts/ci.contract.ts +2 -2
- package/src/contracts/claude.contract.ts +3 -3
- package/src/contracts/drafts.contract.ts +1 -1
- package/src/contracts/endpoints.contract.ts +2 -2
- package/src/contracts/exit.contract.ts +42 -0
- package/src/contracts/extensions.contract.ts +8 -8
- package/src/contracts/git.contract.ts +6 -6
- package/src/contracts/grok.contract.ts +4 -4
- package/src/contracts/host.contract.ts +5 -5
- package/src/contracts/intentic.contract.ts +2 -2
- package/src/contracts/logs.contract.ts +1 -1
- package/src/contracts/loops.contract.ts +8 -8
- package/src/contracts/personas.contract.ts +8 -8
- package/src/contracts/ports.contract.ts +1 -1
- package/src/contracts/prepush.contract.ts +2 -2
- package/src/contracts/providers.contract.ts +4 -4
- package/src/contracts/public.contract.ts +1 -1
- package/src/contracts/push.contract.ts +2 -2
- package/src/contracts/secrets.contract.ts +2 -2
- package/src/contracts/sessions.contract.ts +2 -2
- package/src/contracts/settings.contract.ts +3 -3
- package/src/contracts/share.contract.ts +1 -1
- package/src/contracts/skills.contract.ts +2 -2
- package/src/contracts/system.contract.ts +9 -9
- package/src/contracts/usage.contract.ts +1 -1
- package/src/contracts/vpn.contract.ts +5 -5
- package/src/contracts/workflows.contract.ts +12 -12
- package/src/contracts/workspace.contract.ts +13 -13
- package/src/conversation-ids.ts +8 -8
- package/src/events.test.ts +3 -3
- package/src/events.ts +142 -142
- package/src/fast-tier.test.ts +88 -0
- package/src/fast-tier.ts +72 -0
- package/src/history-state.ts +15 -15
- package/src/host-protocol.ts +7 -7
- package/src/hostnames.test.ts +1 -1
- package/src/hostnames.ts +15 -15
- package/src/index.ts +13 -8
- package/src/listener-protocol.ts +12 -12
- package/src/model-order.test.ts +11 -11
- package/src/model-order.ts +52 -30
- package/src/output-fields.ts +11 -11
- package/src/path-refs.test.ts +4 -4
- package/src/path-refs.ts +6 -6
- package/src/prompt-complexity.test.ts +160 -0
- package/src/prompt-complexity.ts +271 -0
- package/src/publish-drafts.ts +8 -8
- package/src/quick-model.test.ts +11 -11
- package/src/quick-model.ts +15 -15
- package/src/routes.test.ts +11 -5
- package/src/routes.ts +19 -19
- package/src/runtime-state.test.ts +1 -1
- package/src/runtime-state.ts +22 -22
- package/src/schemas.test.ts +8 -8
- package/src/schemas.ts +1238 -921
- package/src/search-globs.test.ts +2 -2
- package/src/search-globs.ts +6 -6
- package/src/session-names.ts +5 -5
- package/src/share-paths.test.ts +1 -1
- package/src/share-paths.ts +7 -7
- package/src/sse.ts +1 -1
- package/src/state-portability.ts +8 -8
- package/src/terminal-protocol.ts +3 -3
- package/src/title.test.ts +9 -9
- package/src/title.ts +21 -21
- package/src/tunnel-ids.test.ts +3 -3
- package/src/tunnel-ids.ts +7 -7
- package/src/versions.test.ts +3 -3
- package/src/versions.ts +6 -6
- package/src/workflow-faults.test.ts +6 -6
- package/src/workflow-faults.ts +14 -14
- package/src/workspace-state.test.ts +32 -32
- package/src/workspace-state.ts +139 -139
|
@@ -13,7 +13,7 @@ describe(`digestOf`, () => {
|
|
|
13
13
|
});
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
/* The anti-drift mechanism. A chore that counts things must not mint a new digest
|
|
16
|
+
/* The anti-drift mechanism. A chore that counts things must not mint a new digest, and therefore a new badge:
|
|
17
17
|
* every time an ordinary day's work moves the number by one. Buckets widen with the count, because the difference
|
|
18
18
|
* between one and two matters and the difference between four hundred and five hundred does not. */
|
|
19
19
|
describe(`bucketOf`, () => {
|
package/src/chores/digest.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/* THE EVIDENCE DIGEST
|
|
1
|
+
/* THE EVIDENCE DIGEST, the one idea that turns a nagging panel into something you can live with.
|
|
2
2
|
*
|
|
3
3
|
* A chore is due because of a measurement. Run the turn, and one of three things happens: the measurement moves
|
|
4
4
|
* (the work landed), it stays put (the work is pending review, or the tool was wrong), or the tool reports
|
|
5
|
-
* something else entirely next week. Only the first is "done", and none of them is answerable by a timestamp
|
|
5
|
+
* something else entirely next week. Only the first is "done", and none of them is answerable by a timestamp,
|
|
6
6
|
* "ran 3 days ago" cannot tell you whether it ran against THIS.
|
|
7
7
|
*
|
|
8
8
|
* So a run records a hash of the evidence that provoked it. The next verdict compares:
|
|
@@ -32,7 +32,7 @@ export const digestOf = (...parts: readonly (string | number)[]): string => {
|
|
|
32
32
|
|
|
33
33
|
/* Buckets a count so that ordinary drift does not read as news. Twelve outdated packages becoming thirteen is not
|
|
34
34
|
* a thing to interrupt someone about; twelve becoming forty is. Powers-of-two boundaries, so the bucket widens
|
|
35
|
-
* with the number
|
|
35
|
+
* with the number, the difference between 1 and 2 matters and the difference between 400 and 500 does not.
|
|
36
36
|
*
|
|
37
37
|
* This is the difference between a digest that changes on every poll (and therefore badges forever) and one that
|
|
38
38
|
* changes when the situation does. Chores that count things digest the BUCKET; chores whose evidence is a set of
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { composeAsk } from "./prompt.js";
|
|
2
2
|
|
|
3
|
-
/* AN EXTENSION UPDATE, READ AS A DIFF. The commit that is installed was approved once already
|
|
3
|
+
/* AN EXTENSION UPDATE, READ AS A DIFF. The commit that is installed was approved once already, re-reading all
|
|
4
4
|
* of it would bury the one question an update asks: what is different, and did any of it change the deal? So
|
|
5
5
|
* the turn's subject is the diff between the two commits, and the manifest's delta leads, because a new entry
|
|
6
6
|
* in `permissions.sandbox` is reach the owner never approved, arriving dressed as an update.
|
|
7
7
|
*
|
|
8
8
|
* Here in the contract's chores rather than in the web app, because two callers build it: the update card's
|
|
9
|
-
* "read the diff" button, and the daemon's agent-prepared update policy
|
|
9
|
+
* "read the diff" button, and the daemon's agent-prepared update policy, which runs this exact read
|
|
10
10
|
* unprompted when the registry lists a new sha, so the owner opens a finished account instead of starting one. */
|
|
11
11
|
const UPDATE_INVARIANTS =
|
|
12
12
|
`This turn reads and reports; it changes nothing and installs nothing. Clone into a scratch directory ` +
|
|
13
|
-
`outside the workspace and read the diff between the two commits
|
|
13
|
+
`outside the workspace and read the diff between the two commits: the installed code was approved once ` +
|
|
14
14
|
`already, so what is between them is the whole subject. Lead with the manifest's delta: any route added to ` +
|
|
15
15
|
`\`permissions.sandbox\` is reach the owner never approved and the headline whatever else changed. Then the ` +
|
|
16
16
|
`code: what behaviour changed, in the owner's terms, citing file and line.`;
|
|
@@ -19,7 +19,7 @@ export interface UpdateBrief {
|
|
|
19
19
|
// The listing's display name, or the repository when it is being installed straight from a URL.
|
|
20
20
|
readonly label: string;
|
|
21
21
|
readonly url: string;
|
|
22
|
-
// What is installed and what the update proposes
|
|
22
|
+
// What is installed and what the update proposes, both full shas, both facts, neither a branch.
|
|
23
23
|
readonly fromRef: string;
|
|
24
24
|
readonly toRef: string;
|
|
25
25
|
// Subdirectory inside the repository, for a monorepo source. Empty for a repo of its own.
|
|
@@ -33,5 +33,5 @@ export const updateBrief = ({ label, url, fromRef, toRef, path }: UpdateBrief):
|
|
|
33
33
|
diagnosis: `The manifest (intentic-extension.json at the extension root) is the contract on both sides of the diff, so its delta is readable exactly like the code's.`,
|
|
34
34
|
goal: `Read the diff and say what the update actually is: the manifest delta first, then what the code now does that it did not, and what it stopped doing.`,
|
|
35
35
|
invariants: UPDATE_INVARIANTS,
|
|
36
|
-
done: `Done when you end on a recommendation the owner can act on
|
|
36
|
+
done: `Done when you end on a recommendation the owner can act on, update, update and watch something named, or stay on ${fromRef.slice(0, 7)}, with the change that decided it cited by file and line.`,
|
|
37
37
|
});
|
package/src/chores/fix-deps.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
/* THE FIX CHORE
|
|
1
|
+
/* THE FIX CHORE, the template for opting into dependency-breakage repair.
|
|
2
2
|
*
|
|
3
3
|
* The automations catalogue offers it as a recipe; nothing creates it until the owner picks that template.
|
|
4
4
|
*
|
|
5
|
-
* It wakes on `deps.broken
|
|
5
|
+
* It wakes on `deps.broken`, the dependency verifier's edge event: a landed change drifted the installed
|
|
6
6
|
* dependencies, the daemon reinstalled them, ran the tree's own checks, and they came back red. The payload
|
|
7
7
|
* is that event: `deps.project` is the project whose checks failed, `deps.command` the exact command that
|
|
8
8
|
* judged it, `deps.exitCode` and `deps.logTail` what it said, and `deps.attempt` which consecutive red this
|
|
9
9
|
* is since the last green.
|
|
10
10
|
*
|
|
11
|
-
* THE GUARD IS THE LOOP CAP, and it lives in guard shell
|
|
11
|
+
* THE GUARD IS THE LOOP CAP, and it lives in guard shell, one visible, owner-editable line, rather than in
|
|
12
12
|
* daemon code, because whoever tunes "how many tries before a human looks" must be able to see the number.
|
|
13
13
|
* Attempt 1 is the breakage, attempt 2 is one landed fix that still failed; past that the loop stops and the
|
|
14
14
|
* standing red is the owner's to read (the activity feed has been narrating every step).
|
|
@@ -28,7 +28,7 @@ export const FIX_DEPS_AUTOMATION = {
|
|
|
28
28
|
"and they failed. The payload names the project (`deps.project`), the check command (`deps.command`), how it exited " +
|
|
29
29
|
"(`deps.exitCode`) and the tail of its output (`deps.logTail`); the full log is in the project's `--verify` terminal.\n\n" +
|
|
30
30
|
"Re-run the check yourself to see the failure first-hand, then fix the ROOT CAUSE. That usually means updating call " +
|
|
31
|
-
"sites, types or tests to match what actually changed
|
|
31
|
+
"sites, types or tests to match what actually changed: never loosening or deleting the checks, pinning or downgrading " +
|
|
32
32
|
"dependencies just to silence them, or editing generated files. If the breakage needs a decision only the owner can " +
|
|
33
33
|
"make (an intentional breaking upgrade, a license change, a dependency that should be dropped), stop and say exactly " +
|
|
34
34
|
"that instead of guessing.\n\n" +
|
|
@@ -3,7 +3,7 @@ import { probeSpec } from "./probes.js";
|
|
|
3
3
|
import { IDIOM_RULES } from "./stack.js";
|
|
4
4
|
|
|
5
5
|
/* The parsers are the part of this library that faces someone else's output, so they are tested the way that
|
|
6
|
-
* output actually arrives: real shapes, then the shapes that have historically broken things
|
|
6
|
+
* output actually arrives: real shapes, then the shapes that have historically broken things, a tool that
|
|
7
7
|
* printed a warning line before its JSON, a version whose fields moved, an empty run. The bar for every one of
|
|
8
8
|
* them is the same: recognise it, or return undefined so the runner can record a failure. Never throw, and never
|
|
9
9
|
* report a clean result from output it did not understand. */
|
|
@@ -135,7 +135,7 @@ describe(`knip`, () => {
|
|
|
135
135
|
});
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
// Without an `issues` array this is not knip's report, whatever else it contains
|
|
138
|
+
// Without an `issues` array this is not knip's report, whatever else it contains, and reporting zero dead code
|
|
139
139
|
// from a shape we do not recognise is exactly the lie the state machine exists to prevent.
|
|
140
140
|
test(`a shape without an issues array is a failure`, () => {
|
|
141
141
|
expect(parse(`knip`, JSON.stringify({ files: [`src/old.ts`] }))).toBeUndefined();
|
|
@@ -233,8 +233,8 @@ describe(`ui`, () => {
|
|
|
233
233
|
});
|
|
234
234
|
|
|
235
235
|
/* Every path the sweep prints wears a `./`, because every ripgrep in it is handed `.` to walk. Downstream this
|
|
236
|
-
* would have to be remembered at each comparison
|
|
237
|
-
* a component
|
|
236
|
+
* would have to be remembered at each comparison: jscpd's paths against the component list, a bypass against
|
|
237
|
+
* a component, so it is spent once, here, and one spelling of a path leaves the parser. */
|
|
238
238
|
test(`strips the prefix ripgrep prints for a path it was told to walk`, () => {
|
|
239
239
|
expect(parse(`ui`, sweep(`COMPONENT\t./src/Button.vue`, `BYPASS\t./src/Button.vue:3`, `IDIOM\tvue-options-api\t./src/Old.vue`))).toEqual({
|
|
240
240
|
id: `ui`,
|
|
@@ -257,7 +257,7 @@ describe(`ui`, () => {
|
|
|
257
257
|
describe(`the sweep's composed command`, () => {
|
|
258
258
|
const stages = (): string[] => probeSpec(`ui`).command.split(`; `);
|
|
259
259
|
|
|
260
|
-
/* Given no path, ripgrep searches STDIN whenever stdin is not a TTY
|
|
260
|
+
/* Given no path, ripgrep searches STDIN whenever stdin is not a TTY, which is exactly how a probe is spawned.
|
|
261
261
|
* The sweep exited 0, printed its marker and matched nothing, in every repository, forever, which the marker
|
|
262
262
|
* line cannot catch because the sweep really did run. It reproduces from a child process and never from an
|
|
263
263
|
* interactive shell, so the command is the only place it is visible. */
|
|
@@ -300,7 +300,7 @@ describe(`bundle`, () => {
|
|
|
300
300
|
});
|
|
301
301
|
|
|
302
302
|
// The `find` prints nothing for a directory that exists but holds no assets. `available` is supposed to catch
|
|
303
|
-
// that, and this is the second line of defence
|
|
303
|
+
// that, and this is the second line of defence: a zero-byte bundle would otherwise read as a fact.
|
|
304
304
|
test(`a directory line with no assets is an empty build, not a failure`, () => {
|
|
305
305
|
expect(parse(`bundle`, `DIR\tbuild`)).toMatchObject({ bundle: { dir: `build`, assets: [], totalBytes: 0, totalGzip: 0 } });
|
|
306
306
|
});
|
package/src/chores/probes.ts
CHANGED
|
@@ -11,19 +11,19 @@ import {
|
|
|
11
11
|
TAILWIND_PACKAGES,
|
|
12
12
|
} from "./stack.js";
|
|
13
13
|
|
|
14
|
-
/* THE PROBES
|
|
14
|
+
/* THE PROBES, the measurements that cost a subprocess, declared once so the daemon that runs them and the panel
|
|
15
15
|
* that explains them cannot disagree about what "outdated" meant.
|
|
16
16
|
*
|
|
17
17
|
* A spec is a shell command and a parser, deliberately in that order of trust: the command is whatever the tool's
|
|
18
18
|
* own maintainers publish as its machine-readable output, and the parser is written to be DISAPPOINTED. Every one
|
|
19
19
|
* of these tools has changed its JSON shape at least once, they are run against whatever version the repo pinned,
|
|
20
20
|
* and a probe that throws on an unexpected field would take the whole maintenance surface down with it. So each
|
|
21
|
-
* parser walks the structure defensively and returns `undefined` when it cannot recognise what it got
|
|
21
|
+
* parser walks the structure defensively and returns `undefined` when it cannot recognise what it got, which the
|
|
22
22
|
* runner records as a failed probe with the output attached, rather than as a clean repository.
|
|
23
23
|
*
|
|
24
24
|
* TIERS ARE ABOUT COST, and the cost is what sets the cadence. Tier 1 reads metadata that already exists (a
|
|
25
25
|
* lockfile, a registry's version list) and finishes in seconds, so the background runner refreshes it daily. Tier
|
|
26
|
-
* 2 reads the whole tree
|
|
26
|
+
* 2 reads the whole tree, knip type-checks it, jscpd tokenizes every file, and can run for minutes on a large
|
|
27
27
|
* repo, so it refreshes weekly and says how long it took, because a reader deciding whether to force a refresh
|
|
28
28
|
* deserves to know what they are asking for.
|
|
29
29
|
*
|
|
@@ -48,7 +48,7 @@ export interface ProbeSpec {
|
|
|
48
48
|
// Exit 0 ⇒ this repo can be measured. Runs in the repo's own directory, like the command.
|
|
49
49
|
readonly available: string;
|
|
50
50
|
/* What is MISSING when `available` says no, named here rather than derived from the title. The obvious
|
|
51
|
-
* derivation
|
|
51
|
+
* derivation, "this repository has no security advisories to measure", states the one thing an unmeasured
|
|
52
52
|
* probe must never claim, that there are none, and it is the same conflation the block above exists to
|
|
53
53
|
* prevent. Phrased as a bare clause ("no lockfile"), because the panel groups these under its own lead-in. */
|
|
54
54
|
readonly unavailable: string;
|
|
@@ -97,7 +97,7 @@ const semverKind = (current: string, latest: string): OutdatedPackage["kind"] =>
|
|
|
97
97
|
/* `pnpm outdated --json` prints a map of package name → { current, latest, dependencyType }. In a workspace the
|
|
98
98
|
* recursive form merges every package's entries into the same map and adds `dependentPackages`, which is why this
|
|
99
99
|
* reads the map rather than expecting a list: one shape covers both, and a field we don't use appearing is not a
|
|
100
|
-
* parse failure. Entries missing `current` or `latest` are skipped
|
|
100
|
+
* parse failure. Entries missing `current` or `latest` are skipped, that is how pnpm reports a package it could
|
|
101
101
|
* not resolve against the registry, and it is not evidence of anything. */
|
|
102
102
|
const parseOutdated = (stdout: string): ProbeFacts | undefined => {
|
|
103
103
|
const root = asObject(stdout);
|
|
@@ -123,8 +123,8 @@ const parseOutdated = (stdout: string): ProbeFacts | undefined => {
|
|
|
123
123
|
const SEVERITIES = new Set([`critical`, `high`, `moderate`, `low`, `info`]);
|
|
124
124
|
|
|
125
125
|
/* `pnpm audit --json` prints `{ advisories: { <id>: {...} }, metadata: {...} }`. The metadata's counts are
|
|
126
|
-
* deliberately ignored: they are a tally, and this surface needs the advisories themselves
|
|
127
|
-
* whether a patched range exists
|
|
126
|
+
* deliberately ignored: they are a tally, and this surface needs the advisories themselves, which package, and
|
|
127
|
+
* whether a patched range exists, because "there is a fix that is a version bump" and "there is no patch yet"
|
|
128
128
|
* lead to completely different turns, and a count cannot tell them apart.
|
|
129
129
|
*
|
|
130
130
|
* `dev` comes off the findings' own flag rather than being inferred. A build-time-only advisory is real but it is
|
|
@@ -135,7 +135,7 @@ const parseAudit = (stdout: string): ProbeFacts | undefined => {
|
|
|
135
135
|
return undefined;
|
|
136
136
|
}
|
|
137
137
|
const raw = root[`advisories`];
|
|
138
|
-
// No `advisories` key at all is pnpm's clean report
|
|
138
|
+
// No `advisories` key at all is pnpm's clean report, an empty list, not an unrecognisable one.
|
|
139
139
|
if (raw === undefined) {
|
|
140
140
|
return { id: `audit`, advisories: [] };
|
|
141
141
|
}
|
|
@@ -168,7 +168,7 @@ const parseAudit = (stdout: string): ProbeFacts | undefined => {
|
|
|
168
168
|
return { id: `audit`, advisories };
|
|
169
169
|
};
|
|
170
170
|
|
|
171
|
-
/* knip's JSON reporter prints `{ issues: [...] }
|
|
171
|
+
/* knip's JSON reporter prints `{ issues: [...] }`, one row per file that has findings, carrying a per-kind array
|
|
172
172
|
* of what it found there. A wholly unreferenced file is a row whose own `files` array names it, which is why that
|
|
173
173
|
* count is a sum like every other kind rather than a list of its own. Counts plus a sample of the file paths, not
|
|
174
174
|
* the full list: the agent re-runs knip itself against the live tree (a list from a probe hours old would send it
|
|
@@ -233,16 +233,16 @@ const parseJscpd = (stdout: string): ProbeFacts | undefined => {
|
|
|
233
233
|
* Everything it emits is a labelled, tab-separated line, and the first line is always the bare marker `UI`. That
|
|
234
234
|
* marker is the whole reason this parser can tell "the sweep ran and this repository is clean" from "the sweep
|
|
235
235
|
* never ran": every other line is optional, so without it an empty stdout and a spotless codebase are the same
|
|
236
|
-
* string
|
|
236
|
+
* string, and reporting the second when it was the first is the one thing probes.ts exists to prevent. */
|
|
237
237
|
const UI_MARKER = `UI`;
|
|
238
|
-
// Caps, applied after `sort` so truncation is alphabetical and therefore identical between runs
|
|
238
|
+
// Caps, applied after `sort` so truncation is alphabetical and therefore identical between runs, an unsorted
|
|
239
239
|
// truncation would mint a new digest on every sweep and badge forever. What is dropped is genuinely dropped: a
|
|
240
240
|
// component past the cap cannot join a family, and the chore says so rather than implying it saw everything.
|
|
241
241
|
const COMPONENT_LIMIT = 2000;
|
|
242
242
|
const RULE_FILE_LIMIT = 500;
|
|
243
243
|
|
|
244
244
|
/* THE `.` IS LOAD-BEARING, and leaving it off cost this probe every finding it will ever have. Given no path,
|
|
245
|
-
* ripgrep searches the tree only when stdin is a TTY
|
|
245
|
+
* ripgrep searches the tree only when stdin is a TTY, otherwise it reads STDIN, which is exactly how the runner
|
|
246
246
|
* spawns a probe. The sweep therefore ran, exited 0, printed its marker and matched nothing, in every repository,
|
|
247
247
|
* forever: the precise failure the marker line was introduced to make impossible, arriving through the one door it
|
|
248
248
|
* does not cover. It reproduces from Node and not from an interactive shell, which is why it survived being read.
|
|
@@ -367,7 +367,7 @@ const parseBundle = (stdout: string): ProbeFacts | undefined => {
|
|
|
367
367
|
};
|
|
368
368
|
|
|
369
369
|
// Where the tier-2 tools leave their reports. Under /tmp because they are inputs to a parse that happens
|
|
370
|
-
// immediately after, never something to keep
|
|
370
|
+
// immediately after, never something to keep, the cached ProbeResult is the artefact that survives. The same
|
|
371
371
|
// path the scheduled form of this chore uses (chores.ts), so a workspace running both keeps one copy.
|
|
372
372
|
const JSCPD_DIR = `/tmp/intentic-chore-jscpd`;
|
|
373
373
|
|
|
@@ -423,7 +423,7 @@ export const PROBES: readonly ProbeSpec[] = [
|
|
|
423
423
|
timeoutMs: 20 * 60_000,
|
|
424
424
|
available: `test -f package.json`,
|
|
425
425
|
unavailable: `no package.json`,
|
|
426
|
-
// `--threshold 100` so jscpd never fails the command on its own opinion of what is too much duplication
|
|
426
|
+
// `--threshold 100` so jscpd never fails the command on its own opinion of what is too much duplication,
|
|
427
427
|
// that judgement is the chore's, made from the percentage, not the tool's exit code.
|
|
428
428
|
command:
|
|
429
429
|
`pnpm dlx jscpd --reporters json --output ${JSCPD_DIR} --min-lines 12 --threshold 100 . >/dev/null 2>&1; ` +
|
|
@@ -437,11 +437,11 @@ export const PROBES: readonly ProbeSpec[] = [
|
|
|
437
437
|
/* Tier 1 despite reading the whole tree, and the placement is a judgement rather than an oversight. The
|
|
438
438
|
* tier is about COST: this is a dozen ripgrep walks, seconds on a large monorepo, against knip
|
|
439
439
|
* type-checking the tree and jscpd tokenizing every file for minutes. A weekly TTL would also make it the
|
|
440
|
-
* wrong shape
|
|
440
|
+
* wrong shape, its findings move whenever someone writes a component, which is daily. */
|
|
441
441
|
tier: 1,
|
|
442
442
|
ttlMs: DAY_MS,
|
|
443
443
|
timeoutMs: 5 * 60_000,
|
|
444
|
-
// Any manifest in the repo declaring a UI framework or Tailwind, not just the root's
|
|
444
|
+
// Any manifest in the repo declaring a UI framework or Tailwind, not just the root's, a monorepo keeps
|
|
445
445
|
// React in the app package and the root manifest is a handful of build tools.
|
|
446
446
|
available:
|
|
447
447
|
`rg -l --no-messages -g '**/package.json' -g '!**/node_modules/**' ` +
|
|
@@ -463,7 +463,7 @@ export const PROBES: readonly ProbeSpec[] = [
|
|
|
463
463
|
available: `find ${BUILD_DIRS.join(` `)} -maxdepth 4 -type f \\( -name '*.js' -o -name '*.mjs' -o -name '*.css' \\) 2>/dev/null | head -n 1 | grep -q .`,
|
|
464
464
|
// Says what is missing AND that this never builds, because the obvious reading of "no build output" is
|
|
465
465
|
// that we tried and it failed. The owner running their own build once is the whole fix.
|
|
466
|
-
unavailable: `no build output on disk
|
|
466
|
+
unavailable: `no build output on disk, this reads the last build, it never runs one`,
|
|
467
467
|
command: bundleCommand(),
|
|
468
468
|
parse: parseBundle,
|
|
469
469
|
},
|
package/src/chores/prompt.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
/* HOW WE ASK. Every prompt this workspace generates from a measurement
|
|
1
|
+
/* HOW WE ASK. Every prompt this workspace generates from a measurement, a hotspot's refactor, a chore's sweep,
|
|
2
2
|
* has the same four parts, in the same order, for the same reasons:
|
|
3
3
|
*
|
|
4
4
|
* subject the one line that says what is being worked on. First, because a model that reads the rationale
|
|
5
5
|
* before the target starts planning against a subject it has not been told yet.
|
|
6
6
|
* why the NUMBERS, quoted exactly as the panel shows them, then what they mean. Exact so the agent and
|
|
7
7
|
* the person are arguing about one set of facts; the agent can and should recount them.
|
|
8
|
-
* goal what shape to move towards
|
|
8
|
+
* goal what shape to move towards, never a design. Whoever generated this prompt has not read the code,
|
|
9
9
|
* so a prescribed solution from out here is a guess wearing an instruction's clothes.
|
|
10
10
|
* done falsifiable, and checkable by the agent itself. The same resident engine that produced the
|
|
11
11
|
* measurement answers `iq` in the agent's own worktree, so "run it again and see" is available and
|
|
12
12
|
* "I have finished" is not something it has to be taken at its word on.
|
|
13
13
|
*
|
|
14
14
|
* The invariants sit between goal and done because they are the constraints on HOW, and they are stated in full
|
|
15
|
-
* every time rather than assumed. Each one is a specific way the turn fails without it
|
|
15
|
+
* every time rather than assumed. Each one is a specific way the turn fails without it, they are here because
|
|
16
16
|
* they were each learned from a diff nobody could review. */
|
|
17
17
|
|
|
18
18
|
export interface Ask {
|
|
@@ -30,16 +30,16 @@ export const composeAsk = ({ subject, why, diagnosis, goal, invariants, done }:
|
|
|
30
30
|
/* Said to every turn a TOOL woke, and the reason the maintenance surface can point agents at tool output at all.
|
|
31
31
|
* A tool reporting N findings is not reporting N problems: knip is confidently wrong about anything reachable
|
|
32
32
|
* from outside the repo, jscpd counts generated files, an advisory in a build-time dependency is not the same
|
|
33
|
-
* risk as one in a running service. A chore that mechanically actions the whole list is worse than no chore
|
|
33
|
+
* risk as one in a running service. A chore that mechanically actions the whole list is worse than no chore,
|
|
34
34
|
* it makes noisy, confident, wrong changes at three in the morning, and the next person has to review a diff
|
|
35
35
|
* whose author had no opinion about it. */
|
|
36
36
|
export const TRIAGE_NOTE =
|
|
37
37
|
`The measurement woke you; it did not decide anything. Read the repository before you touch it, and treat every ` +
|
|
38
38
|
`finding as a claim to verify rather than a task to execute. If a finding is wrong, say why in one line and leave ` +
|
|
39
|
-
`it
|
|
39
|
+
`it. A run that verifies ten and fixes two is a good run.`;
|
|
40
40
|
|
|
41
41
|
/* The invariants for a turn that CHANGES things. Whatever it does lands as uncommitted work in the owner's
|
|
42
|
-
* workspace, so it is reviewed as one diff by someone who did not watch it happen
|
|
42
|
+
* workspace, so it is reviewed as one diff by someone who did not watch it happen, which is what every clause
|
|
43
43
|
* here is protecting.
|
|
44
44
|
*
|
|
45
45
|
* "Separately explainable" is doing the most work: a chore that fixes its findings AND tidies what it passed on
|
|
@@ -57,8 +57,8 @@ export const REPORT_INVARIANTS =
|
|
|
57
57
|
`can act on or dismiss. Where you would propose an edit, describe it and where it would go instead of making it.`;
|
|
58
58
|
|
|
59
59
|
/* The invariants for a turn refactoring ONE FILE, as the codebase-health panel's rows ask for. Distinct from the
|
|
60
|
-
* chore ones because the
|
|
60
|
+
* chore ones because the scope of changes is the thing at stake: named as a scope rather than "only this file",
|
|
61
61
|
* since half those archetypes ask for new files and must not read as forbidding them. */
|
|
62
62
|
export const REFACTOR_INVARIANTS =
|
|
63
|
-
`Read it first. Behaviour stays identical
|
|
64
|
-
`importers that must follow
|
|
63
|
+
`Read it first. Behaviour stays identical. Changes affect only this file, whatever it splits into, and the ` +
|
|
64
|
+
`importers that must follow. Leave no re-export shims behind.`;
|
package/src/chores/stack.test.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { componentStem, frameworksOf, IDIOM_RULES, idiomRule, UI_FRAMEWORKS, use
|
|
|
5
5
|
*
|
|
6
6
|
* Most of what is below guards a failure that CANNOT be seen by reading the table: a pattern is interpolated into
|
|
7
7
|
* a shell command that runs on someone else's machine at three in the morning, so a stray quote is not a typo
|
|
8
|
-
* anyone reviews
|
|
8
|
+
* anyone reviews: it is a probe that dies in a workspace nobody is watching, with a shell error for a reason. */
|
|
9
9
|
|
|
10
10
|
describe(`the patterns are safe to interpolate`, () => {
|
|
11
11
|
// The scan wraps every pattern and glob in shell single quotes. One apostrophe inside ends the quoting and
|
|
@@ -29,7 +29,7 @@ describe(`the patterns are safe to interpolate`, () => {
|
|
|
29
29
|
|
|
30
30
|
/* Rust's regex crate has no lookaround, and getting it means ripgrep's -P, which is a compile-time option on
|
|
31
31
|
* the box the sweep happens to run on. A rule that seems to need one is asking a question about the FILE
|
|
32
|
-
* rather than about a line
|
|
32
|
+
* rather than about a line, which is what `absent` is. */
|
|
33
33
|
test(`no pattern uses a lookaround`, () => {
|
|
34
34
|
for (const rule of IDIOM_RULES) {
|
|
35
35
|
expect(rule.pattern, rule.id).not.toMatch(/\(\?<?[=!]/);
|
|
@@ -109,7 +109,7 @@ describe(`the name two components share`, () => {
|
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
/* The trap in stripping a trailing number. `H1` and `H2` are different components and reduce to the same
|
|
112
|
-
* single letter, so the stem is only accepted when what survives is still long enough to mean something
|
|
112
|
+
* single letter, so the stem is only accepted when what survives is still long enough to mean something:
|
|
113
113
|
* otherwise the untouched name is kept and the two stay apart. */
|
|
114
114
|
test(`short names keep their digits rather than collapsing together`, () => {
|
|
115
115
|
expect(componentStem(`src/type/H1.tsx`)).toBe(`h1`);
|
package/src/chores/stack.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* WHAT THIS REPOSITORY IS BUILT WITH, and the patterns that follow from it
|
|
1
|
+
/* WHAT THIS REPOSITORY IS BUILT WITH, and the patterns that follow from it, the table the UI chores are written
|
|
2
2
|
* against, kept apart from both the probe that runs it and the chores that read it.
|
|
3
3
|
*
|
|
4
4
|
* It sits in the middle on purpose. probes.ts composes ONE ripgrep sweep out of the rules below, and chores.ts
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
* THE PATTERNS ARE RIPGREP'S DIALECT, and they carry two constraints that are not obvious from reading them:
|
|
14
14
|
*
|
|
15
15
|
* No literal apostrophe, ever. The scan command wraps each pattern in shell single quotes, so a `'` inside one
|
|
16
|
-
* would end the quoting and hand the rest of the regex to the shell. Match quotes as `[\x22\x27]` instead
|
|
16
|
+
* would end the quoting and hand the rest of the regex to the shell. Match quotes as `[\x22\x27]` instead,
|
|
17
17
|
* Rust's regex crate reads those escapes, and the shell never sees a quote character at all. stack.test.ts
|
|
18
18
|
* enforces this, because the failure is a probe that dies at three in the morning in someone else's workspace
|
|
19
19
|
* rather than anything a reader would notice here.
|
|
20
20
|
*
|
|
21
21
|
* No lookaround. Rust's regex crate has none, and reaching for ripgrep's PCRE2 mode to get it would make the
|
|
22
22
|
* sweep depend on how the box's ripgrep was compiled. A rule that seems to need it is usually asking a question
|
|
23
|
-
* about the FILE rather than about a line
|
|
23
|
+
* about the FILE rather than about a line, see `absent` below, which is what that question actually is. */
|
|
24
24
|
|
|
25
25
|
export interface UiFramework {
|
|
26
26
|
readonly id: string;
|
|
@@ -38,7 +38,7 @@ export const UI_FRAMEWORKS: readonly UiFramework[] = [
|
|
|
38
38
|
{ id: `angular`, label: `Angular`, packages: [`@angular/core`] },
|
|
39
39
|
];
|
|
40
40
|
|
|
41
|
-
// Tailwind is not in the table above because it is not a UI framework and does not own any idiom rules
|
|
41
|
+
// Tailwind is not in the table above because it is not a UI framework and does not own any idiom rules, it is a
|
|
42
42
|
// styling system that any of the three can be wearing, and it gates exactly one chore.
|
|
43
43
|
export const TAILWIND_PACKAGES: readonly string[] = [`tailwindcss`];
|
|
44
44
|
|
|
@@ -64,20 +64,20 @@ export const SCAN_IGNORES: readonly string[] = [
|
|
|
64
64
|
`!**/*.{test,spec,stories}.*`,
|
|
65
65
|
];
|
|
66
66
|
|
|
67
|
-
// What counts as a component file, across all three frameworks at once. The sweep cannot vary by repository
|
|
68
|
-
// probe's command is a fixed string
|
|
67
|
+
// What counts as a component file, across all three frameworks at once. The sweep cannot vary by repository, a
|
|
68
|
+
// probe's command is a fixed string, so it asks for all of them and a Vue-only repo simply has no `.tsx` files.
|
|
69
69
|
export const COMPONENT_GLOBS: readonly string[] = [`*.vue`, `*.tsx`, `*.jsx`, `*.component.ts`];
|
|
70
70
|
|
|
71
71
|
// Where a Tailwind class can appear. Wider than COMPONENT_GLOBS because a class list lives in markup as often as
|
|
72
|
-
// in a component
|
|
72
|
+
// in a component, an Angular template and a plain .html page both style with the same utilities.
|
|
73
73
|
export const MARKUP_GLOBS: readonly string[] = [`*.vue`, `*.tsx`, `*.jsx`, `*.html`, `*.svelte`, `*.astro`];
|
|
74
74
|
|
|
75
|
-
/* THE DESIGN SYSTEM BYPASS. Not "any arbitrary value"
|
|
75
|
+
/* THE DESIGN SYSTEM BYPASS. Not "any arbitrary value", `grid-cols-[1fr_auto]` and `w-[calc(100%-2rem)]` are
|
|
76
76
|
* Tailwind working as designed, and a chore that counted them would be objecting to the feature rather than to
|
|
77
77
|
* anything wrong. What this matches is the two arbitrary values that route around a decision the theme already
|
|
78
78
|
* made: a colour that is not in the palette, and a pixel size that is not on the spacing or type scale.
|
|
79
79
|
*
|
|
80
|
-
* The leading `-` is
|
|
80
|
+
* The leading `-` is required. It anchors the match to a utility prefix (`bg-`, `text-`, `w-`), so a bare
|
|
81
81
|
* `[...]` in ordinary prose or an array index cannot be mistaken for a class. */
|
|
82
82
|
export const BYPASS_PATTERN = `-\\[(#[0-9a-fA-F]{3,8}|(rgb|hsl)a?\\(|[0-9]+(\\.[0-9]+)?px)`;
|
|
83
83
|
|
|
@@ -87,12 +87,12 @@ export interface IdiomRule {
|
|
|
87
87
|
readonly framework: string;
|
|
88
88
|
// What the repository still has, named as the reader would name it.
|
|
89
89
|
readonly label: string;
|
|
90
|
-
// What replaced it. Carried so the prompt can say where to go rather than only what to leave
|
|
90
|
+
// What replaced it. Carried so the prompt can say where to go rather than only what to leave, an agent told
|
|
91
91
|
// "you still use NgModule" and nothing else will pick a destination, and it may not pick this one.
|
|
92
92
|
readonly replacement: string;
|
|
93
93
|
readonly pattern: string;
|
|
94
94
|
readonly globs: readonly string[];
|
|
95
|
-
/* THE IDIOM IS THE PATTERN BEING MISSING, not present
|
|
95
|
+
/* THE IDIOM IS THE PATTERN BEING MISSING, not present, `pattern` names the NEW way, and the file is on the
|
|
96
96
|
* old one precisely because the new one does not appear in it anywhere. The sweep spells this
|
|
97
97
|
* `--files-without-match`.
|
|
98
98
|
*
|
|
@@ -100,7 +100,7 @@ export interface IdiomRule {
|
|
|
100
100
|
* <script setup>" was first written as a lookahead over `<script`, which matches per LINE: a migrated
|
|
101
101
|
* component with a second plain `<script>` block for defineOptions, or one that merely mentions `<script` in
|
|
102
102
|
* a comment, both read as un-migrated. It reported five files in an application whose 167 SFCs are every one
|
|
103
|
-
* of them migrated. The question was never "is there a line like this"
|
|
103
|
+
* of them migrated. The question was never "is there a line like this", it is "does this file contain the
|
|
104
104
|
* new idiom at all", which is one flag rather than a cleverer regex, and it costs no PCRE2.
|
|
105
105
|
*
|
|
106
106
|
* The globs carry more weight on an absent rule than on a normal one, and narrowly is the only safe way to
|
|
@@ -110,7 +110,7 @@ export interface IdiomRule {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/* THE IDIOMS THEIR OWN MAINTAINERS HAVE MOVED ON FROM. Every rule here names something the framework's own
|
|
113
|
-
* documentation now steers people away from, and every one of them still works
|
|
113
|
+
* documentation now steers people away from, and every one of them still works, which is exactly why they
|
|
114
114
|
* accumulate, and why no editor and no linter will bring them up unprompted.
|
|
115
115
|
*
|
|
116
116
|
* High confidence over coverage. Each pattern is one a reader can check by eye against a file, and the ones that
|
|
@@ -156,7 +156,7 @@ export const IDIOM_RULES: readonly IdiomRule[] = [
|
|
|
156
156
|
framework: `vue`,
|
|
157
157
|
label: `the Options API`,
|
|
158
158
|
replacement: `<script setup> with the Composition API`,
|
|
159
|
-
// The new idiom, inverted by `absent` below
|
|
159
|
+
// The new idiom, inverted by `absent` below, an SFC that never opens a `<script setup>` tag is still on
|
|
160
160
|
// the old one. A file with no script block at all is swept up too, and that is the honest reading: it has
|
|
161
161
|
// not been migrated because there was nothing there to migrate.
|
|
162
162
|
pattern: `<script[^>]*\\bsetup\\b`,
|
|
@@ -213,14 +213,14 @@ export const idiomRule = (id: string): IdiomRule | undefined => IDIOM_RULES.find
|
|
|
213
213
|
* side had two extra characters. */
|
|
214
214
|
export const normalizePath = (path: string): string => path.replace(/^\.\//, ``);
|
|
215
215
|
|
|
216
|
-
// Below this a stem is too short to have survived the stripping above with its meaning intact
|
|
216
|
+
// Below this a stem is too short to have survived the stripping above with its meaning intact, `H1` and `H2`
|
|
217
217
|
// would both reduce to `h` and read as one family of heading components that are not duplicates of anything.
|
|
218
218
|
const MIN_STEM = 3;
|
|
219
219
|
|
|
220
220
|
const QUALIFIER_PREFIX = /^(base|the)/;
|
|
221
221
|
const QUALIFIER_SUFFIX = /(v[0-9]+|new|old|legacy|copy|component|[0-9]+)$/;
|
|
222
222
|
|
|
223
|
-
/* THE NAME TWO COMPONENTS SHARE WHEN THEY ARE THE SAME COMPONENT TWICE
|
|
223
|
+
/* THE NAME TWO COMPONENTS SHARE WHEN THEY ARE THE SAME COMPONENT TWICE, or `undefined` when the file has no
|
|
224
224
|
* name worth comparing.
|
|
225
225
|
*
|
|
226
226
|
* This is a normaliser, not a similarity score, and that is the point: it answers a question the reader can check
|
|
@@ -232,7 +232,7 @@ const QUALIFIER_SUFFIX = /(v[0-9]+|new|old|legacy|copy|component|[0-9]+)$/;
|
|
|
232
232
|
* forty index files is a finding about the naming convention rather than about any duplication. */
|
|
233
233
|
export const componentStem = (path: string): string | undefined => {
|
|
234
234
|
const file = normalizePath(path).split(`/`).pop() ?? ``;
|
|
235
|
-
// `.component.ts` loses both suffixes, `.vue` loses one
|
|
235
|
+
// `.component.ts` loses both suffixes, `.vue` loses one, taking everything before the first dot handles both
|
|
236
236
|
// without a table, since a component's name is never the part after a dot.
|
|
237
237
|
const base = (file.split(`.`)[0] ?? ``).toLowerCase().replace(/[^a-z0-9]/g, ``);
|
|
238
238
|
if (base === `` || base === `index`) {
|