mandrel 2.56.0 → 2.58.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/.agents/agents/plan-critic.md +13 -18
- package/.agents/agents/story-worker.md +25 -33
- package/.agents/docs/agentrc-reference.json +0 -30
- package/.agents/docs/configuration.md +8 -28
- package/.agents/docs/execution-reference.md +5 -5
- package/.agents/docs/quality-gates.md +8 -7
- package/.agents/instructions.md +9 -10
- package/.agents/schemas/agentrc.schema.json +9 -185
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +107 -17
- package/.agents/scripts/ceremony-derive.js +191 -0
- package/.agents/scripts/check-context-budget.js +28 -33
- package/.agents/scripts/check-cyclomatic.js +4 -3
- package/.agents/scripts/deliver-light.js +31 -94
- package/.agents/scripts/evidence-gate.js +17 -1
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
- package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
- package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
- package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
- package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
- package/.agents/scripts/lib/close-validation/gates.js +52 -1
- package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
- package/.agents/scripts/lib/config/delivery-routing.js +7 -33
- package/.agents/scripts/lib/config/explain.js +0 -19
- package/.agents/scripts/lib/config/limits.js +18 -78
- package/.agents/scripts/lib/config/quality.js +6 -3
- package/.agents/scripts/lib/config/runners.js +3 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
- package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
- package/.agents/scripts/lib/config-settings-schema.js +16 -143
- package/.agents/scripts/lib/crap-engine.js +35 -4
- package/.agents/scripts/lib/crap-utils.js +17 -1
- package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
- package/.agents/scripts/lib/orchestration/code-review.js +7 -3
- package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
- package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
- package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
- package/.agents/scripts/lib/orchestration/pinned-identifier-lint.js +137 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +189 -387
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
- package/.agents/scripts/lib/orchestration/plan-persist/acceptance-handle-repair.js +107 -0
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +305 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +138 -170
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +128 -297
- package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +36 -135
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
- package/.agents/scripts/lib/orchestration/review-base-ref.js +138 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +37 -5
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +6 -1
- package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
- package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
- package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
- package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
- package/.agents/scripts/lib/story-body/story-body.js +54 -240
- package/.agents/scripts/lib/templates/decomposer-prompts.js +133 -121
- package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
- package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
- package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
- package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
- package/.agents/scripts/lib/test-run-credit.js +277 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
- package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
- package/.agents/scripts/lib/workers/crap-worker.js +32 -41
- package/.agents/scripts/plan-context.js +7 -9
- package/.agents/scripts/plan-critics.js +28 -54
- package/.agents/scripts/plan-persist.js +25 -68
- package/.agents/scripts/quality-preview.js +51 -0
- package/.agents/scripts/run-tests.js +12 -0
- package/.agents/scripts/stories-wave-tick.js +23 -45
- package/.agents/scripts/test-isolate.js +13 -180
- package/.agents/scripts/update-coverage-baseline.js +25 -70
- package/.agents/scripts/update-crap-baseline.js +19 -123
- package/.agents/skills/core/scope-triage/SKILL.md +3 -3
- package/.agents/workflows/audit-clean-code.md +4 -3
- package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
- package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
- package/.agents/workflows/helpers/code-review.md +2 -3
- package/.agents/workflows/helpers/deliver-digest.md +46 -55
- package/.agents/workflows/helpers/deliver-light.md +40 -105
- package/.agents/workflows/helpers/deliver-reference.md +1 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +54 -55
- package/.agents/workflows/helpers/deliver-story.md +10 -13
- package/.agents/workflows/helpers/plan-reference.md +163 -221
- package/.agents/workflows/mandrel-plan.md +31 -40
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +36 -0
- package/lib/cli/registry.js +98 -2
- package/lib/migrations/index.js +4 -0
- package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
- package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
- package/package.json +1 -1
- package/.agents/scripts/lib/framework-version.js +0 -39
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
- package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
- package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* the Stage-1 split-policy validator (`assertAcceptancePartition`).
|
|
8
8
|
*
|
|
9
9
|
* Each Story body is the single executable document: Tech Spec stays inline
|
|
10
|
-
* under `## Spec
|
|
11
|
-
*
|
|
10
|
+
* under `## Spec`, at whatever length the work needs (Story #5312 deleted
|
|
11
|
+
* the token budget that used to refuse it) — never spilled to `docs/`.
|
|
12
|
+
* Top-level `acceptance[]` / `verify[]` are the machine
|
|
12
13
|
* contract and are synced into the body so the GitHub issue stays complete
|
|
13
14
|
* without requiring the LLM to dual-author the same lists.
|
|
14
15
|
*
|
|
@@ -33,7 +34,6 @@ import {
|
|
|
33
34
|
concurrentMap,
|
|
34
35
|
FANOUT_CONCURRENCY,
|
|
35
36
|
} from '../../util/concurrent-map.js';
|
|
36
|
-
import { assertSpecWithinBudget } from '../spec-spill.js';
|
|
37
37
|
import { assertAcceptancePartition } from '../split-policy-validator.js';
|
|
38
38
|
import {
|
|
39
39
|
externalDependencyId,
|
|
@@ -60,13 +60,6 @@ export const PLAN_RUN_LABEL_PREFIX = 'plan-run::';
|
|
|
60
60
|
/** Stable color for the cohort grouping label (`ensureLabels`). */
|
|
61
61
|
const PLAN_RUN_LABEL_COLOR = '#C5DEF5';
|
|
62
62
|
|
|
63
|
-
/**
|
|
64
|
-
* Stable color for the `route::lite` ceremony-route hint (Story #4707;
|
|
65
|
-
* hint-only since Story #4722 — `/mandrel-deliver` re-derives the route from the
|
|
66
|
-
* Story body's shape).
|
|
67
|
-
*/
|
|
68
|
-
const LITE_ROUTE_LABEL_COLOR = '#D4C5F9';
|
|
69
|
-
|
|
70
63
|
/** Length of the derived plan-run id (hex chars). */
|
|
71
64
|
const PLAN_RUN_ID_LENGTH = 8;
|
|
72
65
|
|
|
@@ -226,11 +219,10 @@ function extractPlanFingerprint(body) {
|
|
|
226
219
|
* Labels the authoring pass is never allowed to set. The `agent::*` axis is
|
|
227
220
|
* the runtime's lifecycle state (persist owns the terminal `agent::ready`
|
|
228
221
|
* flip itself), `type::*` is fixed to `type::story` by the v2 hierarchy,
|
|
229
|
-
* `persona::*`
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* itself.
|
|
222
|
+
* `persona::*` and `route::*` are retired axes (the latter with the
|
|
223
|
+
* plan-side lite claim, Story #5312), and `plan-run::*` is the
|
|
224
|
+
* runtime-derived cohort grouping axis (Story #4692) — a hand-authored entry
|
|
225
|
+
* on it would compete with the deterministic label persist applies itself.
|
|
234
226
|
*/
|
|
235
227
|
const FORBIDDEN_LABEL_PREFIXES = Object.freeze([
|
|
236
228
|
'agent::',
|
|
@@ -299,8 +291,6 @@ function bodyObjectFromTicket(ticket) {
|
|
|
299
291
|
verify: ticket.verify ?? [],
|
|
300
292
|
references: ticket.references ?? [],
|
|
301
293
|
non_goals: ticket.non_goals ?? [],
|
|
302
|
-
wide: ticket.wide ?? null,
|
|
303
|
-
reason_to_exist: ticket.reason_to_exist ?? null,
|
|
304
294
|
depends_on: ticket.depends_on ?? [],
|
|
305
295
|
}).body;
|
|
306
296
|
}
|
|
@@ -398,7 +388,7 @@ export function normalizeStoryTicket(ticket) {
|
|
|
398
388
|
|
|
399
389
|
/**
|
|
400
390
|
* Fold optional shared Tech Spec prose into a Story body when the Story has
|
|
401
|
-
* no inline Spec. Specs stay inline
|
|
391
|
+
* no inline Spec. Specs stay inline, verbatim, at any length.
|
|
402
392
|
*
|
|
403
393
|
* Precedence: per-Story `body.spec` wins; otherwise `sharedSpec` is used
|
|
404
394
|
* (N===1 convenience only — callers must not share one Spec across N>1).
|
|
@@ -430,8 +420,7 @@ export function foldSpecIntoStoryBody(bodyObject, slug, opts = {}) {
|
|
|
430
420
|
return { bodyObject: next };
|
|
431
421
|
}
|
|
432
422
|
|
|
433
|
-
|
|
434
|
-
next.spec = content;
|
|
423
|
+
next.spec = inline;
|
|
435
424
|
return { bodyObject: next };
|
|
436
425
|
}
|
|
437
426
|
|
|
@@ -854,16 +843,14 @@ async function mirrorNativeDependencyEdges({ provider, stories, idBySlug }) {
|
|
|
854
843
|
}
|
|
855
844
|
|
|
856
845
|
/**
|
|
857
|
-
* Ensure
|
|
858
|
-
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
846
|
+
* Ensure the runtime-derived `plan-run::<id>` cohort label exists before it
|
|
847
|
+
* is applied — GitHub's create-issue path does not auto-create unknown
|
|
848
|
+
* labels on every provider route, and an opaque derived label never exists
|
|
849
|
+
* yet.
|
|
861
850
|
*
|
|
862
851
|
* **Non-fatal by design**, matching the native-blocked_by mirroring posture:
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
* route from the Story body's shape, Story #4722), so it is never a reason
|
|
866
|
-
* to fail persist. On an ensure
|
|
852
|
+
* the label is not load-bearing for correctness (grouping is cosmetic), so
|
|
853
|
+
* it is never a reason to fail persist. On an ensure
|
|
867
854
|
* failure (throw, or the label reported `missing` by the post-loop
|
|
868
855
|
* reconcile) the create loop proceeds **without** the label — applying an
|
|
869
856
|
* unensured label could fail the issue create itself, and the Stories matter
|
|
@@ -957,30 +944,16 @@ async function ensurePersistLabel({
|
|
|
957
944
|
* every id is known (Story #4544), so plan-created order stops depending on
|
|
958
945
|
* prose. That pass is non-fatal — see `mirrorNativeDependencyEdges`.
|
|
959
946
|
*
|
|
960
|
-
* **A lite-routed cohort carries the `route::lite` hint** (Story #4707,
|
|
961
|
-
* hint-only since Story #4722). When the caller resolves the plan's
|
|
962
|
-
* effective complexity route to `lite` (the planner's recorded verdict,
|
|
963
|
-
* upheld by the shape backstop), it passes the label via `opts.routeLabel`
|
|
964
|
-
* and every created Story carries it — a **human-visible hint only**, never
|
|
965
|
-
* the control signal: `/mandrel-deliver` re-derives the route from each Story body's
|
|
966
|
-
* own shape (`resolveStoryDispatchMode`), so a lost or failed label write
|
|
967
|
-
* cannot misroute delivery. A full-routed plan passes nothing and its
|
|
968
|
-
* Stories carry **no** route label. Like the cohort label, the ensure is
|
|
969
|
-
* non-fatal — the label is cosmetic either way.
|
|
970
|
-
*
|
|
971
947
|
* @param {object} args
|
|
972
948
|
* @param {object} args.provider
|
|
973
949
|
* @param {ReturnType<typeof assemblePlanStories>['stories']} args.stories
|
|
974
950
|
* @param {object} [args.opts]
|
|
975
951
|
* @param {boolean} [args.opts.dryRun=false]
|
|
976
|
-
* @param {string|null} [args.opts.routeLabel=null] Route marker label to
|
|
977
|
-
* apply to every created Story (`route::lite`), or null for none.
|
|
978
952
|
* @returns {Promise<{
|
|
979
953
|
* created: Array<{ slug: string, id: number, url?: string, title: string, adopted: boolean }>,
|
|
980
954
|
* dependencyEdges: { edgesAdded: number, edgesSkipped: number, edgesFailed: number, storiesProcessed: number }|null,
|
|
981
955
|
* planRunLabel: string,
|
|
982
956
|
* planRunLabelApplied: boolean,
|
|
983
|
-
* routeLabel: string|null,
|
|
984
957
|
* }>}
|
|
985
958
|
*/
|
|
986
959
|
export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
@@ -991,10 +964,6 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
991
964
|
}
|
|
992
965
|
|
|
993
966
|
const list = Array.isArray(stories) ? stories : [];
|
|
994
|
-
const routeLabel =
|
|
995
|
-
typeof opts.routeLabel === 'string' && opts.routeLabel.trim() !== ''
|
|
996
|
-
? opts.routeLabel.trim()
|
|
997
|
-
: null;
|
|
998
967
|
|
|
999
968
|
// Derived once for the whole cohort, before any write — a pure function of
|
|
1000
969
|
// the authored artifacts, so dry-run can report it write-free and a resume
|
|
@@ -1018,7 +987,6 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
1018
987
|
// the label — the derived id is still reported, the application is not
|
|
1019
988
|
// claimed.
|
|
1020
989
|
planRunLabelApplied: false,
|
|
1021
|
-
routeLabel,
|
|
1022
990
|
};
|
|
1023
991
|
}
|
|
1024
992
|
|
|
@@ -1031,18 +999,6 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
1031
999
|
'metadata only, never a deliver input.',
|
|
1032
1000
|
role: 'cohort',
|
|
1033
1001
|
});
|
|
1034
|
-
const applyRouteLabel =
|
|
1035
|
-
routeLabel !== null &&
|
|
1036
|
-
(await ensurePersistLabel({
|
|
1037
|
-
provider,
|
|
1038
|
-
label: routeLabel,
|
|
1039
|
-
color: LITE_ROUTE_LABEL_COLOR,
|
|
1040
|
-
description:
|
|
1041
|
-
'Ceremony-lite hint only: /mandrel-deliver re-derives the route; ' +
|
|
1042
|
-
'every close gate still runs.',
|
|
1043
|
-
role: 'route-marker',
|
|
1044
|
-
}));
|
|
1045
|
-
|
|
1046
1002
|
const { byFingerprint, idsByTitle } = await indexExistingStories(provider);
|
|
1047
1003
|
const created = [];
|
|
1048
1004
|
const idBySlug = new Map();
|
|
@@ -1073,11 +1029,7 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
1073
1029
|
const result = await provider.createIssue({
|
|
1074
1030
|
title: story.title,
|
|
1075
1031
|
body: renderStoryBodyForCreate(story, idBySlug),
|
|
1076
|
-
labels: [
|
|
1077
|
-
...story.labels,
|
|
1078
|
-
...(applyCohortLabel ? [cohortLabel] : []),
|
|
1079
|
-
...(applyRouteLabel ? [routeLabel] : []),
|
|
1080
|
-
],
|
|
1032
|
+
labels: [...story.labels, ...(applyCohortLabel ? [cohortLabel] : [])],
|
|
1081
1033
|
// Story #5112 — hand the provider the same content-keyed lookup this
|
|
1082
1034
|
// loop's resume path uses, so a retry after a lost response adopts the
|
|
1083
1035
|
// issue attempt 1 already filed instead of creating a twin.
|
|
@@ -1128,7 +1080,6 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
1128
1080
|
// consult before advertising a `label:` filter that may match nothing.
|
|
1129
1081
|
planRunLabel: cohortLabel,
|
|
1130
1082
|
planRunLabelApplied: applyCohortLabel,
|
|
1131
|
-
routeLabel: applyRouteLabel ? routeLabel : null,
|
|
1132
1083
|
};
|
|
1133
1084
|
}
|
|
1134
1085
|
|
|
@@ -10,23 +10,19 @@
|
|
|
10
10
|
* @module lib/orchestration/plan-persist/wave-serialisation
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
detectCollision,
|
|
15
|
-
OVERLAP_SOURCES,
|
|
16
|
-
renderScrapeAttribution,
|
|
17
|
-
} from '../../wave-runner/footprint.js';
|
|
13
|
+
import { detectCollision } from '../../wave-runner/footprint.js';
|
|
18
14
|
|
|
19
15
|
/**
|
|
20
16
|
* Predict which same-wave pairs the dispatch guard will actually refuse to
|
|
21
17
|
* co-dispatch (Story #5265).
|
|
22
18
|
*
|
|
23
19
|
* The wave table answers a `depends_on` question, and the runtime answers a
|
|
24
|
-
* different one: `stories-wave-tick.js` withholds on {@link detectCollision}
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
20
|
+
* different one: `stories-wave-tick.js` withholds on {@link detectCollision}
|
|
21
|
+
* over the declared `changes[]` (Story #5313 retired the text scrape), so
|
|
22
|
+
* two same-wave Stories that both declare a path — a shared generated
|
|
23
|
+
* baseline, say — are shown in one order and dispatched one at a time. The
|
|
24
|
+
* table promised parallelism the next tick refused, with nothing anywhere
|
|
25
|
+
* reconciling the two.
|
|
30
26
|
*
|
|
31
27
|
* This runs the runtime's own exported predicate — not a reimplementation of
|
|
32
28
|
* it — pairwise within each wave, so the prediction cannot drift from the
|
|
@@ -34,8 +30,8 @@ import {
|
|
|
34
30
|
*
|
|
35
31
|
* @param {ReturnType<typeof buildWaveTable>} waveTable
|
|
36
32
|
* @param {Array<{ slug: string, title?: string, body?: string, spec?: string, changes?: Array }>} stories
|
|
37
|
-
* @param {{ tempRoot?: string }} [options]
|
|
38
|
-
* @returns {Array<{ wave: number, slugs: [string, string], paths: string[], source: string
|
|
33
|
+
* @param {{ tempRoot?: string }} [options] Accepted for compatibility; unread.
|
|
34
|
+
* @returns {Array<{ wave: number, slugs: [string, string], paths: string[], source: string }>}
|
|
39
35
|
*/
|
|
40
36
|
export function predictWaveSerialisation(waveTable, stories, options = {}) {
|
|
41
37
|
const bySlug = new Map(
|
|
@@ -71,8 +67,7 @@ export function predictWaveSerialisation(waveTable, stories, options = {}) {
|
|
|
71
67
|
* against the same promise, and an operator should read them the same way.
|
|
72
68
|
* The difference is what they know — the shared-editor pass names paths two
|
|
73
69
|
* Stories both *write*, this one names every pair the dispatcher will refuse
|
|
74
|
-
* to run together
|
|
75
|
-
* path was scraped out of a `## Verify` line.
|
|
70
|
+
* to run together, glob declarations included.
|
|
76
71
|
*
|
|
77
72
|
* @param {ReturnType<typeof predictWaveSerialisation>} collisions
|
|
78
73
|
* @returns {string[]}
|
|
@@ -80,31 +75,23 @@ export function predictWaveSerialisation(waveTable, stories, options = {}) {
|
|
|
80
75
|
export function renderPredictedSerialisationLines(collisions) {
|
|
81
76
|
const list = Array.isArray(collisions) ? collisions : [];
|
|
82
77
|
if (list.length === 0) return [];
|
|
83
|
-
const rows = list.map(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const scrapedOnly = list.filter(
|
|
90
|
-
(c) => c.source === OVERLAP_SOURCES.SCRAPED,
|
|
91
|
-
).length;
|
|
78
|
+
const rows = list.map(
|
|
79
|
+
(c) =>
|
|
80
|
+
`| \`${c.slugs[0]}\` + \`${c.slugs[1]}\` | ${c.paths
|
|
81
|
+
.map((p) => `\`${p}\``)
|
|
82
|
+
.join(', ')} | ${c.source} |`,
|
|
83
|
+
);
|
|
92
84
|
return [
|
|
93
85
|
'',
|
|
94
86
|
`#### ⚠️ Predicted serialisation (${list.length} same-order pair(s))`,
|
|
95
87
|
'',
|
|
96
|
-
'| Stories | Colliding paths | Overlap source |
|
|
97
|
-
'| --- | --- | --- |
|
|
88
|
+
'| Stories | Colliding paths | Overlap source |',
|
|
89
|
+
'| --- | --- | --- |',
|
|
98
90
|
...rows,
|
|
99
91
|
'',
|
|
100
|
-
'_The dispatcher compares the **
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
|
|
104
|
-
'scraped paths; the "Scraped from" column names the field each such ' +
|
|
105
|
-
'path was read out of, so a shared `## Verify` command is ' +
|
|
106
|
-
'distinguishable from a genuine unpredicted edit target. The guard is ' +
|
|
107
|
-
'deliberately not narrowed to `changes[]`: the declaration is a lower ' +
|
|
108
|
-
'bound (Story #4875) and under-serialising is the worse failure._',
|
|
92
|
+
'_The dispatcher compares the **declared** `changes[]` footprints ' +
|
|
93
|
+
'(Story #5313 retired the text scrape), so these pairs are shown in ' +
|
|
94
|
+
'one order above but will be dispatched one at a time: both Stories ' +
|
|
95
|
+
'declare a colliding path, or one declares a glob._',
|
|
109
96
|
];
|
|
110
97
|
}
|
|
@@ -1,33 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* plan-text-hygiene.js —
|
|
3
|
-
*
|
|
2
|
+
* plan-text-hygiene.js — the advisory draft-Story lints: `open-question`
|
|
3
|
+
* over body prose (Story #4599; narrowed to one lint by Story #5312) and
|
|
4
|
+
* `pinned-identifier` over `acceptance[]` (Story #5323).
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* This module makes those classes checkable at the one point a re-author
|
|
11
|
-
* loop exists — the pre-persist critic gate (`plan-critics.js`).
|
|
6
|
+
* A Story is executed by a non-interactive sub-agent, so an operator-directed
|
|
7
|
+
* open question persisted into its body ("Flag if…", "TBD", "confirm with the
|
|
8
|
+
* operator", a trailing `?`) can never be answered where it is read. This
|
|
9
|
+
* module makes that class checkable at the one point a re-author loop exists —
|
|
10
|
+
* the persist dry-run, which lists every match as a **warning** and proceeds.
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* and
|
|
12
|
+
* Story #5312 deleted the sibling `dangling-citation` and `slicing-mass`
|
|
13
|
+
* lints with the critic gate that surfaced them: both scored prose shape the
|
|
14
|
+
* authoring model already judges, and neither ever changed a persisted body.
|
|
16
15
|
*
|
|
17
|
-
*
|
|
16
|
+
* `pinned-identifier` is the one lint that scores the **binding** half of the
|
|
17
|
+
* ticket; its classifier lives in
|
|
18
|
+
* [`pinned-identifier-lint.js`](pinned-identifier-lint.js), which shares no
|
|
19
|
+
* vocabulary with the prose heuristic here.
|
|
18
20
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* `#<digits>` issue anchor in the same sentence. An anchor written
|
|
22
|
-
* inside a code span counts — the conventional markdown form (Story
|
|
23
|
-
* #4906).
|
|
24
|
-
* - **open-question** — interrogative-to-operator phrasing ("Flag if",
|
|
25
|
-
* "TBD", "confirm with the operator", a trailing `?`) in Goal/Spec
|
|
26
|
-
* prose outside code spans. Bodies record decisions; unresolved
|
|
27
|
-
* unknowns belong in declarative Key Assumptions.
|
|
28
|
-
* - **slicing-mass** — `## Slicing` character mass exceeding `## Spec`
|
|
29
|
-
* character mass when both are present: checkpoints carrying
|
|
30
|
-
* Spec-grade detail the Spec then re-covers.
|
|
21
|
+
* Advisory by contract: findings are deterministic text for the dry-run's
|
|
22
|
+
* warning list. They never gate persist and spawn nothing.
|
|
31
23
|
*
|
|
32
24
|
* Pure, synchronous, no I/O. Operates on the draft `stories.json` array,
|
|
33
25
|
* reusing `parse()` from `lib/story-body/story-body.js` for section access.
|
|
@@ -38,23 +30,11 @@
|
|
|
38
30
|
*/
|
|
39
31
|
|
|
40
32
|
import { parse } from '../story-body/story-body.js';
|
|
33
|
+
import { findPinnedIdentifiers } from './pinned-identifier-lint.js';
|
|
41
34
|
|
|
42
35
|
/** Truncation length for the `evidence` excerpt on each finding. */
|
|
43
36
|
const EVIDENCE_MAX_CHARS = 160;
|
|
44
37
|
|
|
45
|
-
/**
|
|
46
|
-
* Phrases the citation heuristic treats as a reference to an external
|
|
47
|
-
* carrier document. Matched case-insensitively within one sentence.
|
|
48
|
-
*/
|
|
49
|
-
const CITATION_MARKERS = [/§/, /\bdesign note\b/i, /\breview doc\b/i];
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Anchors that locate a citation: a `#<digits>` issue reference or a
|
|
53
|
-
* repo-relative path (a slash-joined token carrying a file-ish segment).
|
|
54
|
-
*/
|
|
55
|
-
const ISSUE_ANCHOR = /#\d+/;
|
|
56
|
-
const REPO_PATH_ANCHOR = /[\w.-]+\/[\w./-]+/;
|
|
57
|
-
|
|
58
38
|
/**
|
|
59
39
|
* Operator-directed open-question phrasings. Each match is an instruction
|
|
60
40
|
* or question aimed at a human, which a non-interactive delivery sub-agent
|
|
@@ -68,62 +48,43 @@ const OPEN_QUESTION_MARKERS = [
|
|
|
68
48
|
|
|
69
49
|
/**
|
|
70
50
|
* @typedef {Object} TextHygieneFinding
|
|
71
|
-
* @property {'
|
|
51
|
+
* @property {'open-question'|'pinned-identifier'} kind
|
|
72
52
|
* @property {string} slug - The draft Story's slug ('' when absent).
|
|
73
53
|
* @property {string} evidence - Excerpt of the offending text.
|
|
74
54
|
* @property {string} message - Human-readable, re-author-actionable text.
|
|
75
55
|
*/
|
|
76
56
|
|
|
77
57
|
/**
|
|
78
|
-
* Private-use sentinel standing in for one extracted inline code span.
|
|
79
|
-
*
|
|
80
|
-
*
|
|
58
|
+
* Private-use sentinel standing in for one extracted inline code span. It
|
|
59
|
+
* carries no question marker and no sentence boundary, so it is inert for
|
|
60
|
+
* the heuristic while keeping the sentence split where the span sat.
|
|
81
61
|
*/
|
|
82
|
-
const
|
|
62
|
+
const CODE_SLOT = '\uE000';
|
|
83
63
|
|
|
84
64
|
/**
|
|
85
65
|
* Replace fenced code blocks with a space and each inline code span with a
|
|
86
66
|
* positional slot, so code content (shell snippets, grep patterns, JSON)
|
|
87
|
-
* never trips a prose heuristic
|
|
67
|
+
* never trips a prose heuristic.
|
|
88
68
|
*
|
|
89
69
|
* @param {string} text
|
|
90
|
-
* @returns {
|
|
70
|
+
* @returns {string}
|
|
91
71
|
*/
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
const slotted = text
|
|
95
|
-
.replace(/```[\s\S]*?```/g, ' ')
|
|
96
|
-
.replace(/`[^`\n]*`/g, (span) => {
|
|
97
|
-
spans.push(span.slice(1, -1));
|
|
98
|
-
return `\uE000${spans.length - 1}\uE001`;
|
|
99
|
-
});
|
|
100
|
-
return { slotted, spans };
|
|
72
|
+
function stripCodeSpans(text) {
|
|
73
|
+
return text.replace(/```[\s\S]*?```/g, ' ').replace(/`[^`\n]*`/g, CODE_SLOT);
|
|
101
74
|
}
|
|
102
75
|
|
|
103
76
|
/**
|
|
104
|
-
* Split prose into sentence-ish units
|
|
105
|
-
* bullet list yields one unit per bullet.
|
|
106
|
-
* the same sentence: `prose`, with code content removed, which every
|
|
107
|
-
* marker heuristic reads; and `anchorText`, with inline code content
|
|
108
|
-
* restored, so a citation anchored inside a code span is still visible to
|
|
109
|
-
* the anchor check. Restoring only ever adds anchors — markers are matched
|
|
110
|
-
* against `prose` alone, exactly as before.
|
|
77
|
+
* Split prose into sentence-ish units with code content removed. Newlines
|
|
78
|
+
* are boundaries too, so a bullet list yields one unit per bullet.
|
|
111
79
|
*
|
|
112
80
|
* @param {string} text
|
|
113
|
-
* @returns {
|
|
81
|
+
* @returns {string[]}
|
|
114
82
|
*/
|
|
115
83
|
function splitSentences(text) {
|
|
116
|
-
|
|
117
|
-
return slotted
|
|
84
|
+
return stripCodeSpans(text)
|
|
118
85
|
.split(/(?<=[.!?])\s+|\n+/)
|
|
119
|
-
.map((unit) => (
|
|
120
|
-
|
|
121
|
-
anchorText: unit.replace(
|
|
122
|
-
CODE_SLOT_PATTERN,
|
|
123
|
-
(_slot, index) => ` ${spans[Number(index)]} `,
|
|
124
|
-
),
|
|
125
|
-
}))
|
|
126
|
-
.filter((unit) => unit.prose.length > 0);
|
|
86
|
+
.map((unit) => unit.replaceAll(CODE_SLOT, ' ').trim())
|
|
87
|
+
.filter((unit) => unit.length > 0);
|
|
127
88
|
}
|
|
128
89
|
|
|
129
90
|
/**
|
|
@@ -139,37 +100,6 @@ function excerpt(text) {
|
|
|
139
100
|
: flat;
|
|
140
101
|
}
|
|
141
102
|
|
|
142
|
-
/**
|
|
143
|
-
* dangling-citation: a citation-marker sentence with no locating anchor.
|
|
144
|
-
* The marker is matched against the sentence's code-stripped prose; the
|
|
145
|
-
* anchor against its code-restored text, so a path or issue reference
|
|
146
|
-
* written in a code span — the conventional markdown form — counts.
|
|
147
|
-
*
|
|
148
|
-
* @param {string} prose - Raw body prose.
|
|
149
|
-
* @param {string} slug
|
|
150
|
-
* @returns {TextHygieneFinding[]}
|
|
151
|
-
*/
|
|
152
|
-
function findDanglingCitations(prose, slug) {
|
|
153
|
-
const findings = [];
|
|
154
|
-
for (const { prose: sentence, anchorText } of splitSentences(prose)) {
|
|
155
|
-
const cites = CITATION_MARKERS.some((m) => m.test(sentence));
|
|
156
|
-
if (!cites) continue;
|
|
157
|
-
const anchored =
|
|
158
|
-
ISSUE_ANCHOR.test(anchorText) || REPO_PATH_ANCHOR.test(anchorText);
|
|
159
|
-
if (anchored) continue;
|
|
160
|
-
findings.push({
|
|
161
|
-
kind: 'dangling-citation',
|
|
162
|
-
slug,
|
|
163
|
-
evidence: excerpt(sentence),
|
|
164
|
-
message:
|
|
165
|
-
'Citation names a document section but no repo-relative path or ' +
|
|
166
|
-
'#<issue> anchor locates it in the same sentence — the executing ' +
|
|
167
|
-
'agent cannot follow it. Anchor the citation or inline the claim.',
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
return findings;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
103
|
/**
|
|
174
104
|
* open-question: operator-directed phrasing (or a trailing `?`) in prose a
|
|
175
105
|
* non-interactive sub-agent executes.
|
|
@@ -180,7 +110,7 @@ function findDanglingCitations(prose, slug) {
|
|
|
180
110
|
*/
|
|
181
111
|
function findOpenQuestions(prose, slug) {
|
|
182
112
|
const findings = [];
|
|
183
|
-
for (const
|
|
113
|
+
for (const sentence of splitSentences(prose)) {
|
|
184
114
|
const marked =
|
|
185
115
|
OPEN_QUESTION_MARKERS.some((m) => m.test(sentence)) ||
|
|
186
116
|
sentence.endsWith('?');
|
|
@@ -200,33 +130,7 @@ function findOpenQuestions(prose, slug) {
|
|
|
200
130
|
}
|
|
201
131
|
|
|
202
132
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* @param {{ slicing?: string, spec?: string }} body - Parsed Story body.
|
|
206
|
-
* @param {string} slug
|
|
207
|
-
* @returns {TextHygieneFinding[]}
|
|
208
|
-
*/
|
|
209
|
-
function findSlicingMass(body, slug) {
|
|
210
|
-
const slicing = typeof body.slicing === 'string' ? body.slicing : '';
|
|
211
|
-
const spec = typeof body.spec === 'string' ? body.spec : '';
|
|
212
|
-
if (slicing.length === 0 || spec.length === 0) return [];
|
|
213
|
-
if (slicing.length <= spec.length) return [];
|
|
214
|
-
return [
|
|
215
|
-
{
|
|
216
|
-
kind: 'slicing-mass',
|
|
217
|
-
slug,
|
|
218
|
-
evidence: excerpt(slicing),
|
|
219
|
-
message:
|
|
220
|
-
`## Slicing (${slicing.length} chars) outweighs ## Spec ` +
|
|
221
|
-
`(${spec.length} chars) — checkpoints are carrying Spec-grade ` +
|
|
222
|
-
'detail. Keep each Slicing checkpoint to one line and move the ' +
|
|
223
|
-
'detail into ## Spec.',
|
|
224
|
-
},
|
|
225
|
-
];
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Evaluate the three text-hygiene lints over a draft Story array.
|
|
133
|
+
* Evaluate the advisory lints over a draft Story array.
|
|
230
134
|
*
|
|
231
135
|
* @param {{ draftStories?: Array<object>|null }} args - The draft
|
|
232
136
|
* `stories.json` array (raw Story objects with top-level `slug` /
|
|
@@ -249,12 +153,9 @@ export function evaluateTextHygiene({ draftStories = null } = {}) {
|
|
|
249
153
|
}
|
|
250
154
|
const goal = typeof body.goal === 'string' ? body.goal : '';
|
|
251
155
|
const spec = typeof body.spec === 'string' ? body.spec : '';
|
|
252
|
-
const bodyProse =
|
|
253
|
-
typeof story.body === 'string' ? story.body : [goal, spec].join('\n');
|
|
254
156
|
findings.push(
|
|
255
|
-
...findDanglingCitations(bodyProse, slug),
|
|
256
157
|
...findOpenQuestions([goal, spec].join('\n'), slug),
|
|
257
|
-
...
|
|
158
|
+
...findPinnedIdentifiers(story, body, slug, excerpt),
|
|
258
159
|
);
|
|
259
160
|
}
|
|
260
161
|
return { findings };
|