@pathmode/cli 2.1.4 → 2.2.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/dist/index.js +757 -50
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16416,6 +16416,44 @@ const gray_matter_1 = __importDefault(__nccwpck_require__(9599));
|
|
|
16416
16416
|
const ajv_1 = __importDefault(__nccwpck_require__(2463));
|
|
16417
16417
|
const api_client_1 = __nccwpck_require__(7475);
|
|
16418
16418
|
const program = new commander_1.Command();
|
|
16419
|
+
/**
|
|
16420
|
+
* The open IntentSpec schema historically described YAML frontmatter, while the
|
|
16421
|
+
* local MCP writer deliberately keeps the editable specification in Markdown
|
|
16422
|
+
* sections. Validate either representation through the same schema: explicit
|
|
16423
|
+
* frontmatter remains authoritative (including invalid values), and parsed body
|
|
16424
|
+
* sections only fill fields that were omitted from frontmatter.
|
|
16425
|
+
*
|
|
16426
|
+
* A body section fills a field only when it actually carried one. The parser is
|
|
16427
|
+
* total — a missing `## Objective` reads back as `''` and missing `## Outcomes`
|
|
16428
|
+
* as `[]` — so filling unconditionally handed the schema a present-but-empty
|
|
16429
|
+
* value and `required` was satisfied by nothing. A spec with no outcomes in
|
|
16430
|
+
* either representation validated clean. Absent in both must stay absent, so the
|
|
16431
|
+
* schema can name the field, which is the same rule constraints, edge cases, and
|
|
16432
|
+
* health metrics already follow below.
|
|
16433
|
+
*/
|
|
16434
|
+
function buildValidationSubject(content) {
|
|
16435
|
+
const { data } = (0, gray_matter_1.default)(content);
|
|
16436
|
+
const parsed = (0, local_reader_1.parseIntentMarkdown)(content);
|
|
16437
|
+
const outcomes = parsed.outcomes.map(outcome => typeof outcome === 'string' ? outcome : outcome.text);
|
|
16438
|
+
return {
|
|
16439
|
+
...data,
|
|
16440
|
+
...(!Object.prototype.hasOwnProperty.call(data, 'objective') && parsed.objective
|
|
16441
|
+
? { objective: parsed.objective }
|
|
16442
|
+
: {}),
|
|
16443
|
+
...(!Object.prototype.hasOwnProperty.call(data, 'outcomes') && outcomes.length > 0
|
|
16444
|
+
? { outcomes }
|
|
16445
|
+
: {}),
|
|
16446
|
+
...(!Object.prototype.hasOwnProperty.call(data, 'constraints') && parsed.constraints.length > 0
|
|
16447
|
+
? { constraints: parsed.constraints }
|
|
16448
|
+
: {}),
|
|
16449
|
+
...(!Object.prototype.hasOwnProperty.call(data, 'edgeCases') && parsed.edgeCases.length > 0
|
|
16450
|
+
? { edgeCases: parsed.edgeCases }
|
|
16451
|
+
: {}),
|
|
16452
|
+
...(!Object.prototype.hasOwnProperty.call(data, 'healthMetrics') && parsed.healthMetrics.length > 0
|
|
16453
|
+
? { healthMetrics: parsed.healthMetrics }
|
|
16454
|
+
: {}),
|
|
16455
|
+
};
|
|
16456
|
+
}
|
|
16419
16457
|
// Detect the GitHub ACTION invocation specifically: action.yml runs dist/index.js with no
|
|
16420
16458
|
// argv and passes INPUT_FILE through the environment. The old check keyed on GITHUB_ACTIONS
|
|
16421
16459
|
// alone, which is set for EVERY step of every workflow, so `pathmode preflight <path>` inside
|
|
@@ -16442,7 +16480,7 @@ const validateIntent = async (file) => {
|
|
|
16442
16480
|
}
|
|
16443
16481
|
try {
|
|
16444
16482
|
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
16445
|
-
const
|
|
16483
|
+
const data = buildValidationSubject(content);
|
|
16446
16484
|
const schema = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, 'schema.json'), 'utf-8'));
|
|
16447
16485
|
const ajv = new ajv_1.default({ allErrors: true });
|
|
16448
16486
|
const validate = ajv.compile(schema);
|
|
@@ -16906,6 +16944,463 @@ else {
|
|
|
16906
16944
|
}
|
|
16907
16945
|
|
|
16908
16946
|
|
|
16947
|
+
/***/ }),
|
|
16948
|
+
|
|
16949
|
+
/***/ 229:
|
|
16950
|
+
/***/ ((__unused_webpack_module, exports) => {
|
|
16951
|
+
|
|
16952
|
+
"use strict";
|
|
16953
|
+
|
|
16954
|
+
/**
|
|
16955
|
+
* serializeIntentMd — the one implementation of the intent.md file format.
|
|
16956
|
+
*
|
|
16957
|
+
* Two writers used to exist: `lib/agentPromptGenerator.ts` (cloud exports, browser downloads) and
|
|
16958
|
+
* `packages/mcp-server/src/intent-compiler.ts` (every local, keyless write). They diverged in BOTH
|
|
16959
|
+
* directions, and the divergence cost more than tidiness:
|
|
16960
|
+
*
|
|
16961
|
+
* - the cloud writer emitted no `## Confirmations`, so a human's authenticated confirmation died
|
|
16962
|
+
* at the boundary and never reached the repo;
|
|
16963
|
+
* - the local writer emitted no authorization gate, so an agent proposal still pending human
|
|
16964
|
+
* judgment lost its DO-NOT-IMPLEMENT banner on the next local save.
|
|
16965
|
+
*
|
|
16966
|
+
* Both are the same failure: judgment recorded in one place not travelling to the other. This
|
|
16967
|
+
* module emits the union, and `serializer-parity.test.ts` holds the two callers to it.
|
|
16968
|
+
*
|
|
16969
|
+
* ZERO IMPORTS, deliberately. The local writer is bundled by ncc into a published npm package that
|
|
16970
|
+
* must run keyless and offline, and the cloud writer is reached from `'use client'` components — so
|
|
16971
|
+
* a single node builtin here (`crypto`, say) would break the browser bundle. Callers compute their
|
|
16972
|
+
* own environment-specific values (id minting, readiness verdicts, evidence resolution) and hand
|
|
16973
|
+
* the results in as plain data.
|
|
16974
|
+
*/
|
|
16975
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
16976
|
+
exports.GENERATED_CAPTIONS = exports.VERIFICATION_NOT_DEFINED_CAPTION = exports.VERIFICATION_FEEDBACK_CAPTION = void 0;
|
|
16977
|
+
exports.renderOutcomeMeasurementDefinition = renderOutcomeMeasurementDefinition;
|
|
16978
|
+
exports.toSerializableChecks = toSerializableChecks;
|
|
16979
|
+
exports.renderAuthorizationGateText = renderAuthorizationGateText;
|
|
16980
|
+
exports.renderConfirmationsBody = renderConfirmationsBody;
|
|
16981
|
+
exports.normalizeCaption = normalizeCaption;
|
|
16982
|
+
exports.isGeneratedCaption = isGeneratedCaption;
|
|
16983
|
+
exports.serializeIntentMd = serializeIntentMd;
|
|
16984
|
+
// ── Small helpers ───────────────────────────────────────────────────────────
|
|
16985
|
+
const VERIFICATION_KIND_LABELS = {
|
|
16986
|
+
fastest: 'Fastest check',
|
|
16987
|
+
'shipped-signal': 'Shipped signal',
|
|
16988
|
+
'regression-guard': 'Regression guard',
|
|
16989
|
+
manual: 'Manual check',
|
|
16990
|
+
test: 'Automated test',
|
|
16991
|
+
};
|
|
16992
|
+
const VERIFICATION_KIND_ORDER = ['fastest', 'shipped-signal', 'regression-guard', 'manual', 'test'];
|
|
16993
|
+
const VERIFICATION_KIND_SET = new Set(VERIFICATION_KIND_ORDER);
|
|
16994
|
+
function nonEmpty(v) {
|
|
16995
|
+
return typeof v === 'string' && v.trim().length > 0;
|
|
16996
|
+
}
|
|
16997
|
+
/** YAML double-quoted scalars. Both writers used to interpolate raw, so a product named `The "Real"
|
|
16998
|
+
* One` produced a file gray-matter could not parse. */
|
|
16999
|
+
function yamlScalar(value) {
|
|
17000
|
+
if (typeof value === 'number')
|
|
17001
|
+
return String(value);
|
|
17002
|
+
return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
17003
|
+
}
|
|
17004
|
+
function outcomeOf(o) {
|
|
17005
|
+
return typeof o === 'string' ? { text: o } : o;
|
|
17006
|
+
}
|
|
17007
|
+
/** Compact, provider-neutral wording shared by every agent-facing export. */
|
|
17008
|
+
function renderOutcomeMeasurementDefinition(measurement) {
|
|
17009
|
+
if (!measurement)
|
|
17010
|
+
return '';
|
|
17011
|
+
const { source, expectation, window } = measurement;
|
|
17012
|
+
const target = `${expectation.operator} ${expectation.target}${expectation.unit ? ` ${expectation.unit}` : ''}`;
|
|
17013
|
+
const windowValue = window.kind === 'rolling'
|
|
17014
|
+
? `${window.kind} ${window.duration}`
|
|
17015
|
+
: window.kind === 'fixed'
|
|
17016
|
+
? `${window.kind} ${window.start}..${window.end}`
|
|
17017
|
+
: window.kind;
|
|
17018
|
+
return [
|
|
17019
|
+
`measure via ${source.provider} ${source.queryRef.kind}:${source.queryRef.id}`,
|
|
17020
|
+
measurement.resultSelector ? `select ${measurement.resultSelector}` : '',
|
|
17021
|
+
`target ${target}`,
|
|
17022
|
+
`window ${windowValue}`,
|
|
17023
|
+
].filter(Boolean).join('; ');
|
|
17024
|
+
}
|
|
17025
|
+
function constraintOf(c) {
|
|
17026
|
+
return typeof c === 'string' ? { text: c } : c;
|
|
17027
|
+
}
|
|
17028
|
+
/** `backed by:` suffix on its own indented bullet. Empty when nothing resolved, so callers append
|
|
17029
|
+
* unconditionally. */
|
|
17030
|
+
function backedBySuffix(refs, indent = ' ') {
|
|
17031
|
+
const clean = (refs || []).filter(nonEmpty);
|
|
17032
|
+
if (!clean.length)
|
|
17033
|
+
return '';
|
|
17034
|
+
return `\n${indent}- backed by: ${clean.join('; ')}`;
|
|
17035
|
+
}
|
|
17036
|
+
/** Read verification uniformly as a check collection: canonical `checks[]` plus the legacy
|
|
17037
|
+
* {manual,unit,e2e} buckets, which are adapted rather than dropped. */
|
|
17038
|
+
function toSerializableChecks(v) {
|
|
17039
|
+
if (!v || typeof v !== 'object')
|
|
17040
|
+
return [];
|
|
17041
|
+
const out = [];
|
|
17042
|
+
for (const c of Array.isArray(v.checks) ? v.checks : []) {
|
|
17043
|
+
if (!nonEmpty(c?.description))
|
|
17044
|
+
continue;
|
|
17045
|
+
out.push({
|
|
17046
|
+
kind: VERIFICATION_KIND_SET.has(String(c.kind)) ? String(c.kind) : 'test',
|
|
17047
|
+
description: c.description.trim(),
|
|
17048
|
+
status: c.status,
|
|
17049
|
+
verifies: c.verifies,
|
|
17050
|
+
});
|
|
17051
|
+
}
|
|
17052
|
+
for (const d of Array.isArray(v.manualChecks) ? v.manualChecks : []) {
|
|
17053
|
+
if (nonEmpty(d))
|
|
17054
|
+
out.push({ kind: 'manual', description: d.trim() });
|
|
17055
|
+
}
|
|
17056
|
+
for (const d of Array.isArray(v.unitTests) ? v.unitTests : []) {
|
|
17057
|
+
if (nonEmpty(d))
|
|
17058
|
+
out.push({ kind: 'test', description: d.trim() });
|
|
17059
|
+
}
|
|
17060
|
+
for (const d of Array.isArray(v.e2eTests) ? v.e2eTests : []) {
|
|
17061
|
+
if (nonEmpty(d))
|
|
17062
|
+
out.push({ kind: 'test', description: d.trim() });
|
|
17063
|
+
}
|
|
17064
|
+
return out;
|
|
17065
|
+
}
|
|
17066
|
+
function groupChecks(v) {
|
|
17067
|
+
const checks = toSerializableChecks(v);
|
|
17068
|
+
return VERIFICATION_KIND_ORDER
|
|
17069
|
+
.map(kind => ({ kind, label: VERIFICATION_KIND_LABELS[kind], checks: checks.filter(c => c.kind === kind) }))
|
|
17070
|
+
.filter(g => g.checks.length > 0);
|
|
17071
|
+
}
|
|
17072
|
+
function checkLine(c) {
|
|
17073
|
+
const verifies = nonEmpty(c.verifies) ? ` (verifies: ${c.verifies})` : '';
|
|
17074
|
+
const status = nonEmpty(c.status) && c.status !== 'unknown' ? ` [${c.status}]` : '';
|
|
17075
|
+
return `${c.description}${verifies}${status}`;
|
|
17076
|
+
}
|
|
17077
|
+
// ── The authorization gate ──────────────────────────────────────────────────
|
|
17078
|
+
/**
|
|
17079
|
+
* The DO-NOT-IMPLEMENT banner for an agent proposal a human has not authorized.
|
|
17080
|
+
*
|
|
17081
|
+
* Lives here, not in either caller, because the local writer used to omit it entirely: a pending
|
|
17082
|
+
* proposal pulled into a repo and re-saved came back without its gate, which is the one thing in
|
|
17083
|
+
* the file an agent must not be able to lose.
|
|
17084
|
+
*
|
|
17085
|
+
* Trailing blank line included, so callers can prepend unconditionally.
|
|
17086
|
+
*/
|
|
17087
|
+
function renderAuthorizationGateText(opts) {
|
|
17088
|
+
if (opts.origin !== 'agent' || opts.authorization === 'authorized')
|
|
17089
|
+
return '';
|
|
17090
|
+
if (opts.authorization === 'rejected') {
|
|
17091
|
+
const note = nonEmpty(opts.authorizationNote) ? ` The human's note: "${opts.authorizationNote}".` : '';
|
|
17092
|
+
return `> **REJECTED BY HUMAN REVIEW — DO NOT IMPLEMENT.** This spec was proposed by an agent and a human rejected it.${note} Revise the proposal per the note, or ask your operator before proceeding.\n\n`;
|
|
17093
|
+
}
|
|
17094
|
+
return `> **PENDING HUMAN AUTHORIZATION — DO NOT IMPLEMENT YET.** This spec was proposed by an agent and no human has authorized it. Ask your operator to review it in Pathmode before building against it.\n\n`;
|
|
17095
|
+
}
|
|
17096
|
+
// ── Confirmations ───────────────────────────────────────────────────────────
|
|
17097
|
+
/**
|
|
17098
|
+
* Render confirmation records as the `## Confirmations` section body.
|
|
17099
|
+
*
|
|
17100
|
+
* Derived fields (`assurance`, `source`) are NOT written: anything read back out of a file is
|
|
17101
|
+
* local-unverified by definition, so persisting a trust level would let a record vouch for itself.
|
|
17102
|
+
* Records outside the dimensions the readiness gate can resolve are dropped rather than emitted,
|
|
17103
|
+
* so an unwaivable dimension never leaves an empty section behind.
|
|
17104
|
+
*/
|
|
17105
|
+
function renderConfirmationsBody(records) {
|
|
17106
|
+
const emittable = (records || []).filter(c => {
|
|
17107
|
+
const dim = String(c?.dimension ?? '').toLowerCase();
|
|
17108
|
+
const kind = String(c?.kind ?? '').toLowerCase();
|
|
17109
|
+
const by = String(c?.by ?? '').toLowerCase();
|
|
17110
|
+
return ['objective', 'outcomes'].includes(dim)
|
|
17111
|
+
&& ['confirmed', 'waived'].includes(kind)
|
|
17112
|
+
&& ['agent', 'human'].includes(by);
|
|
17113
|
+
});
|
|
17114
|
+
if (!emittable.length)
|
|
17115
|
+
return '';
|
|
17116
|
+
const out = ['## Confirmations'];
|
|
17117
|
+
for (const c of emittable) {
|
|
17118
|
+
out.push('');
|
|
17119
|
+
out.push(`**${String(c.dimension).toLowerCase()}** — ${String(c.kind).toLowerCase()} by ${String(c.by).toLowerCase()}`);
|
|
17120
|
+
for (const key of ['actor', 'problem', 'outcome', 'observable', 'reason', 'anchor', 'at']) {
|
|
17121
|
+
const v = c[key];
|
|
17122
|
+
if (!nonEmpty(v))
|
|
17123
|
+
continue;
|
|
17124
|
+
out.push(`- ${key}: ${v.replace(/\s+/g, ' ').trim()}`);
|
|
17125
|
+
}
|
|
17126
|
+
}
|
|
17127
|
+
return out.join('\n');
|
|
17128
|
+
}
|
|
17129
|
+
// ── The serializer ──────────────────────────────────────────────────────────
|
|
17130
|
+
/**
|
|
17131
|
+
* Frontmatter key order. Fixed so a file rewritten by the other implementation produces a clean
|
|
17132
|
+
* diff instead of a reordering churn that hides the real change.
|
|
17133
|
+
*/
|
|
17134
|
+
const FRONTMATTER_ORDER = [
|
|
17135
|
+
'id', 'version', 'status', 'readiness', 'source', 'specVersion',
|
|
17136
|
+
'origin', 'authorization', 'authorizationNote', 'outcomeMeasurements',
|
|
17137
|
+
'evidence', 'space', 'severity', 'created', 'updated',
|
|
17138
|
+
];
|
|
17139
|
+
/**
|
|
17140
|
+
* Captions this codebase writes into an intent.md, as opposed to anything an author wrote.
|
|
17141
|
+
*
|
|
17142
|
+
* A caption is prose under a heading that is NOT content: it explains the section rather than
|
|
17143
|
+
* recording a decision. The readers exclude exactly these strings, which is why they live here,
|
|
17144
|
+
* next to the code that emits them, and are imported rather than retyped. Excluding "all italic
|
|
17145
|
+
* prose" instead was wrong: an author writing `*Must not slow down page load.*` under
|
|
17146
|
+
* Constraints means it, and reporting that section as absent is the accusation the four-state
|
|
17147
|
+
* gate exists to stop making.
|
|
17148
|
+
*
|
|
17149
|
+
* Adding a caption anywhere means adding it here, or the reader will grade it as content.
|
|
17150
|
+
*/
|
|
17151
|
+
exports.VERIFICATION_FEEDBACK_CAPTION = '_A feedback loop, not just a test list._';
|
|
17152
|
+
/** Written by lib/scan/draftIntentMd.ts when a pre-account draft has no verification yet. */
|
|
17153
|
+
exports.VERIFICATION_NOT_DEFINED_CAPTION = '_Not defined yet. Nothing in this spec has been checked against a running system._';
|
|
17154
|
+
exports.GENERATED_CAPTIONS = [
|
|
17155
|
+
exports.VERIFICATION_FEEDBACK_CAPTION,
|
|
17156
|
+
exports.VERIFICATION_NOT_DEFINED_CAPTION,
|
|
17157
|
+
];
|
|
17158
|
+
/** Trim, drop one layer of surrounding `_` or `*`, casefold. Both emphasis spellings match. */
|
|
17159
|
+
function normalizeCaption(line) {
|
|
17160
|
+
return line
|
|
17161
|
+
.trim()
|
|
17162
|
+
.replace(/^([_*])(.*)\1$/s, '$2')
|
|
17163
|
+
.trim()
|
|
17164
|
+
.toLowerCase();
|
|
17165
|
+
}
|
|
17166
|
+
const NORMALIZED_CAPTIONS = new Set(exports.GENERATED_CAPTIONS.map(normalizeCaption));
|
|
17167
|
+
/** True only for a caption this codebase generated. Author prose, italic or not, is content. */
|
|
17168
|
+
function isGeneratedCaption(line) {
|
|
17169
|
+
return NORMALIZED_CAPTIONS.has(normalizeCaption(line));
|
|
17170
|
+
}
|
|
17171
|
+
function serializeIntentMd(spec, opts = {}) {
|
|
17172
|
+
const outcomeMeasurements = (spec.outcomes || [])
|
|
17173
|
+
.map(outcomeOf)
|
|
17174
|
+
.map((outcome, index) => outcome.measurement ? { outcome: index, ...outcome.measurement } : null)
|
|
17175
|
+
.filter((entry) => entry !== null);
|
|
17176
|
+
const values = {
|
|
17177
|
+
id: spec.id,
|
|
17178
|
+
version: opts.version && opts.version >= 1 ? opts.version : 1,
|
|
17179
|
+
status: opts.status || 'draft',
|
|
17180
|
+
readiness: opts.readiness,
|
|
17181
|
+
source: opts.source,
|
|
17182
|
+
specVersion: opts.specVersion,
|
|
17183
|
+
origin: opts.origin,
|
|
17184
|
+
// Only meaningful for agent-originated specs; human-authored ones are authorized by
|
|
17185
|
+
// authorship and carrying a key here would imply a gate that does not exist.
|
|
17186
|
+
authorization: opts.origin === 'agent' ? (opts.authorization ?? 'pending') : undefined,
|
|
17187
|
+
// The banner renders the note as prose, but prose is not recoverable: a reader cannot tell
|
|
17188
|
+
// the reviewer's words from the sentence around them. Persisting it here is what stops a
|
|
17189
|
+
// local rewrite from keeping the REJECTED verdict while erasing the correction it asked
|
|
17190
|
+
// for. Single-line by contract, like a confirmation value; flattened rather than escaped.
|
|
17191
|
+
authorizationNote: opts.origin === 'agent' && nonEmpty(opts.authorizationNote)
|
|
17192
|
+
? opts.authorizationNote.replace(/\s+/g, ' ').trim()
|
|
17193
|
+
: undefined,
|
|
17194
|
+
// Body outcomes stay plain, interoperable list items. The optional recipe is a generic
|
|
17195
|
+
// extension in frontmatter, keyed by outcome position so old readers simply ignore it.
|
|
17196
|
+
outcomeMeasurements: outcomeMeasurements.length ? outcomeMeasurements : undefined,
|
|
17197
|
+
evidence: opts.evidence?.length || undefined,
|
|
17198
|
+
space: opts.space?.name,
|
|
17199
|
+
severity: opts.severity,
|
|
17200
|
+
created: opts.created,
|
|
17201
|
+
updated: opts.updated,
|
|
17202
|
+
};
|
|
17203
|
+
// null, not just undefined: the cloud mapper reads absent columns as null (`problemSeverity`,
|
|
17204
|
+
// `currentState`, a product with no name), and an unguarded null reached the quoter as a
|
|
17205
|
+
// crash rather than an omitted key.
|
|
17206
|
+
const yamlLines = FRONTMATTER_ORDER
|
|
17207
|
+
.filter(key => values[key] !== undefined && values[key] !== null && values[key] !== '')
|
|
17208
|
+
.map(key => {
|
|
17209
|
+
const value = values[key];
|
|
17210
|
+
return `${key}: ${typeof value === 'object' ? JSON.stringify(value) : yamlScalar(value)}`;
|
|
17211
|
+
})
|
|
17212
|
+
.join('\n');
|
|
17213
|
+
const sections = ['---', yamlLines, '---', ''];
|
|
17214
|
+
// Directly under the frontmatter: the `authorization` key is machine-readable, but the body
|
|
17215
|
+
// must also say DO NOT IMPLEMENT before any instruction an agent might act on.
|
|
17216
|
+
const gate = renderAuthorizationGateText(opts).trim();
|
|
17217
|
+
if (gate) {
|
|
17218
|
+
sections.push(gate);
|
|
17219
|
+
sections.push('');
|
|
17220
|
+
}
|
|
17221
|
+
sections.push(`# ${spec.title || 'Untitled Intent'}`);
|
|
17222
|
+
const space = opts.space;
|
|
17223
|
+
if (space && (nonEmpty(space.productVision) || nonEmpty(space.northStar) || nonEmpty(space.targetAudience)
|
|
17224
|
+
|| space.constraints?.length || space.principles?.length)) {
|
|
17225
|
+
sections.push('');
|
|
17226
|
+
sections.push('## Space Context');
|
|
17227
|
+
if (nonEmpty(space.productVision))
|
|
17228
|
+
sections.push(`**Product Vision**: ${space.productVision}`);
|
|
17229
|
+
if (nonEmpty(space.northStar))
|
|
17230
|
+
sections.push(`**North Star**: ${space.northStar}`);
|
|
17231
|
+
if (nonEmpty(space.targetAudience))
|
|
17232
|
+
sections.push(`**Target Audience**: ${space.targetAudience}`);
|
|
17233
|
+
if (space.constraints?.length)
|
|
17234
|
+
sections.push('**Constraints**: ' + space.constraints.join(', '));
|
|
17235
|
+
if (space.principles?.length)
|
|
17236
|
+
sections.push('**Principles**: ' + space.principles.join(', '));
|
|
17237
|
+
}
|
|
17238
|
+
if (nonEmpty(spec.objective)) {
|
|
17239
|
+
sections.push('');
|
|
17240
|
+
sections.push('## Objective');
|
|
17241
|
+
sections.push(spec.objective);
|
|
17242
|
+
}
|
|
17243
|
+
// Heading text is the round-trip key (the reader keys sections by exact heading) — keep it
|
|
17244
|
+
// plain, exactly like Objective above.
|
|
17245
|
+
if (nonEmpty(spec.currentState)) {
|
|
17246
|
+
sections.push('');
|
|
17247
|
+
sections.push('## Current State');
|
|
17248
|
+
sections.push(spec.currentState.trim());
|
|
17249
|
+
}
|
|
17250
|
+
// Sits next to Current State on purpose: one is what the author says is true today, the other
|
|
17251
|
+
// is what the repo says. Same heading in both modes, so a handoff reads identically either way.
|
|
17252
|
+
const icBody = implementationContextBody(spec);
|
|
17253
|
+
if (icBody.length) {
|
|
17254
|
+
sections.push('');
|
|
17255
|
+
sections.push('## Implementation Context');
|
|
17256
|
+
sections.push(...icBody);
|
|
17257
|
+
}
|
|
17258
|
+
const decisions = (spec.decisions || []).filter(d => d && nonEmpty(d.choice) && typeof d.reason === 'string');
|
|
17259
|
+
if (decisions.length) {
|
|
17260
|
+
sections.push('');
|
|
17261
|
+
sections.push('## Decisions & Ruled-Out Alternatives');
|
|
17262
|
+
for (const d of decisions) {
|
|
17263
|
+
sections.push(`- **${d.choice}**${nonEmpty(d.ruledOut) ? ` (instead of: ${d.ruledOut})` : ''} — ${d.reason}`);
|
|
17264
|
+
if (nonEmpty(d.reopenTrigger))
|
|
17265
|
+
sections.push(` - reopen if: ${d.reopenTrigger.trim()}`);
|
|
17266
|
+
}
|
|
17267
|
+
}
|
|
17268
|
+
const outcomes = (spec.outcomes || []).map(outcomeOf).filter(o => nonEmpty(o.text));
|
|
17269
|
+
if (outcomes.length) {
|
|
17270
|
+
sections.push('');
|
|
17271
|
+
sections.push('## Outcomes');
|
|
17272
|
+
// No priority prefix. The writer used to emit `[MUST] `, which is in neither SPEC.md nor the
|
|
17273
|
+
// normalization corpus, and neither parser strips it — so it read back as part of the
|
|
17274
|
+
// outcome text. Emitting a label the format cannot read back is worse than omitting it.
|
|
17275
|
+
for (const o of outcomes) {
|
|
17276
|
+
sections.push(`- [ ] ${o.text}${backedBySuffix(o.backedBy)}`);
|
|
17277
|
+
}
|
|
17278
|
+
}
|
|
17279
|
+
if (spec.scope?.inScope?.length || spec.scope?.outOfScope?.length) {
|
|
17280
|
+
sections.push('');
|
|
17281
|
+
sections.push('## Scope');
|
|
17282
|
+
if (spec.scope.inScope?.length) {
|
|
17283
|
+
sections.push('**In scope:**');
|
|
17284
|
+
for (const s of spec.scope.inScope)
|
|
17285
|
+
sections.push(`- ${s}`);
|
|
17286
|
+
}
|
|
17287
|
+
if (spec.scope.outOfScope?.length) {
|
|
17288
|
+
sections.push('**Out of scope:**');
|
|
17289
|
+
for (const s of spec.scope.outOfScope)
|
|
17290
|
+
sections.push(`- ${s}`);
|
|
17291
|
+
}
|
|
17292
|
+
}
|
|
17293
|
+
const constraints = (spec.constraints || []).map(constraintOf).filter(c => nonEmpty(c.text));
|
|
17294
|
+
if (constraints.length) {
|
|
17295
|
+
sections.push('');
|
|
17296
|
+
sections.push('## Constraints');
|
|
17297
|
+
for (const c of constraints)
|
|
17298
|
+
sections.push(`- ${c.text}${backedBySuffix(c.backedBy)}`);
|
|
17299
|
+
}
|
|
17300
|
+
const edgeCases = (spec.edgeCases || []).filter(ec => ec && nonEmpty(ec.scenario));
|
|
17301
|
+
if (edgeCases.length) {
|
|
17302
|
+
sections.push('');
|
|
17303
|
+
sections.push('## Edge Cases');
|
|
17304
|
+
for (const ec of edgeCases) {
|
|
17305
|
+
sections.push(`- **${ec.scenario}**: ${ec.expectedBehavior}${backedBySuffix(ec.backedBy)}`);
|
|
17306
|
+
}
|
|
17307
|
+
}
|
|
17308
|
+
const evidenceBody = renderCompactEvidence(opts.evidence);
|
|
17309
|
+
if (evidenceBody.length) {
|
|
17310
|
+
sections.push('');
|
|
17311
|
+
sections.push('## Supporting Evidence');
|
|
17312
|
+
sections.push(...evidenceBody);
|
|
17313
|
+
}
|
|
17314
|
+
if (spec.healthMetrics?.length) {
|
|
17315
|
+
sections.push('');
|
|
17316
|
+
sections.push('## Health Metrics');
|
|
17317
|
+
for (const metric of spec.healthMetrics)
|
|
17318
|
+
sections.push(`- ${metric}`);
|
|
17319
|
+
}
|
|
17320
|
+
const checkGroups = groupChecks(spec.verification);
|
|
17321
|
+
if (checkGroups.length) {
|
|
17322
|
+
sections.push('');
|
|
17323
|
+
sections.push('## Verification');
|
|
17324
|
+
sections.push(exports.VERIFICATION_FEEDBACK_CAPTION);
|
|
17325
|
+
for (const g of checkGroups) {
|
|
17326
|
+
sections.push(`**${g.label}**:`);
|
|
17327
|
+
for (const c of g.checks)
|
|
17328
|
+
sections.push(`- [ ] ${checkLine(c)}`);
|
|
17329
|
+
}
|
|
17330
|
+
}
|
|
17331
|
+
// `## Confirmations` is emitted LAST so it never sits between the fields a reader is comparing,
|
|
17332
|
+
// and so appending one cannot shift any other section's parse.
|
|
17333
|
+
const confirmationsBody = renderConfirmationsBody(spec.confirmations);
|
|
17334
|
+
if (confirmationsBody) {
|
|
17335
|
+
sections.push('');
|
|
17336
|
+
sections.push(confirmationsBody);
|
|
17337
|
+
}
|
|
17338
|
+
return sections.join('\n');
|
|
17339
|
+
}
|
|
17340
|
+
/**
|
|
17341
|
+
* The body of `## Implementation Context`.
|
|
17342
|
+
*
|
|
17343
|
+
* Two sources, one section. Local mode carries prose the agent gathered; cloud intents carry the
|
|
17344
|
+
* analyzer's structured object. Rendering both under the same heading is what lets a spec move
|
|
17345
|
+
* between modes without the section disappearing on the way through.
|
|
17346
|
+
*/
|
|
17347
|
+
function implementationContextBody(spec) {
|
|
17348
|
+
if (nonEmpty(spec.implementationContextText))
|
|
17349
|
+
return [spec.implementationContextText.trim()];
|
|
17350
|
+
const ic = spec.implementationContext;
|
|
17351
|
+
if (!ic)
|
|
17352
|
+
return [];
|
|
17353
|
+
const lines = [];
|
|
17354
|
+
if (ic.relevantAreas?.length) {
|
|
17355
|
+
lines.push('### Relevant areas');
|
|
17356
|
+
for (const a of ic.relevantAreas) {
|
|
17357
|
+
if (nonEmpty(a?.path))
|
|
17358
|
+
lines.push(`- \`${a.path}\`${nonEmpty(a.reason) ? ` — ${a.reason}` : ''}`);
|
|
17359
|
+
}
|
|
17360
|
+
}
|
|
17361
|
+
if (nonEmpty(ic.currentBehavior)) {
|
|
17362
|
+
if (lines.length)
|
|
17363
|
+
lines.push('');
|
|
17364
|
+
lines.push('### Current behavior');
|
|
17365
|
+
lines.push(ic.currentBehavior.trim());
|
|
17366
|
+
}
|
|
17367
|
+
if (ic.risks?.length) {
|
|
17368
|
+
if (lines.length)
|
|
17369
|
+
lines.push('');
|
|
17370
|
+
lines.push('### Risks');
|
|
17371
|
+
for (const r of ic.risks)
|
|
17372
|
+
if (nonEmpty(r))
|
|
17373
|
+
lines.push(`- ${r.trim()}`);
|
|
17374
|
+
}
|
|
17375
|
+
if (ic.verificationSuggestions?.length) {
|
|
17376
|
+
if (lines.length)
|
|
17377
|
+
lines.push('');
|
|
17378
|
+
lines.push('### Verification suggestions');
|
|
17379
|
+
for (const v of ic.verificationSuggestions)
|
|
17380
|
+
if (nonEmpty(v))
|
|
17381
|
+
lines.push(`- ${v.trim()}`);
|
|
17382
|
+
}
|
|
17383
|
+
return lines;
|
|
17384
|
+
}
|
|
17385
|
+
/** Compact evidence list — one truncated line per item, quotes as blockquotes. Lighter than the
|
|
17386
|
+
* execution prompt's full rendering so a committed intent.md stays readable. */
|
|
17387
|
+
function renderCompactEvidence(evidence) {
|
|
17388
|
+
const items = (evidence || []).filter(e => nonEmpty(e?.content));
|
|
17389
|
+
if (!items.length)
|
|
17390
|
+
return [];
|
|
17391
|
+
const lines = [];
|
|
17392
|
+
const truncate = (s) => (s.length > 160 ? `${s.slice(0, 160)}…` : s);
|
|
17393
|
+
for (const q of items.filter(e => e.type === 'quote')) {
|
|
17394
|
+
lines.push(`> "${truncate(q.content)}"${nonEmpty(q.source) ? ` — ${q.source}` : ''}`);
|
|
17395
|
+
}
|
|
17396
|
+
for (const e of items.filter(e => e.type !== 'quote')) {
|
|
17397
|
+
const severity = nonEmpty(e.severity) ? ` (${e.severity})` : '';
|
|
17398
|
+
lines.push(`- [${e.type}]${severity} ${truncate(e.content)}${nonEmpty(e.source) ? ` — ${e.source}` : ''}`);
|
|
17399
|
+
}
|
|
17400
|
+
return lines;
|
|
17401
|
+
}
|
|
17402
|
+
|
|
17403
|
+
|
|
16909
17404
|
/***/ }),
|
|
16910
17405
|
|
|
16911
17406
|
/***/ 8963:
|
|
@@ -16936,6 +17431,7 @@ const fs_1 = __importDefault(__nccwpck_require__(9896));
|
|
|
16936
17431
|
const path_1 = __importDefault(__nccwpck_require__(6928));
|
|
16937
17432
|
const gray_matter_1 = __importDefault(__nccwpck_require__(2702));
|
|
16938
17433
|
const measurement_schema_1 = __nccwpck_require__(2642);
|
|
17434
|
+
const serializeIntentMd_1 = __nccwpck_require__(229);
|
|
16939
17435
|
const CONFIRMATION_HEADER_RE = /^\*\*(objective|outcomes)\*\*\s*[—–-]\s*(confirmed|waived)\s+by\s+(agent|human)\s*$/i;
|
|
16940
17436
|
const CONFIRMATION_FIELD_RE = /^\s*[-*]\s+(actor|problem|outcome|observable|reason|anchor|at)\s*:\s*(.+)$/i;
|
|
16941
17437
|
/**
|
|
@@ -17109,7 +17605,6 @@ function parseIntentMarkdown(content, fallbackId = 'intent') {
|
|
|
17109
17605
|
const { data, content: rawBody } = (0, gray_matter_1.default)(content);
|
|
17110
17606
|
const body = stripHtmlComments(stripFencedBlocks(rawBody));
|
|
17111
17607
|
const sections = splitSections(body);
|
|
17112
|
-
const parsedVerification = extractVerification(sections);
|
|
17113
17608
|
// The public schema allows verification as a flat string array; read it as manual checks
|
|
17114
17609
|
// rather than silently producing {} (the same adapter normalizeSpecForReadiness applies).
|
|
17115
17610
|
const frontmatterVerification = Array.isArray(data.verification)
|
|
@@ -17117,9 +17612,16 @@ function parseIntentMarkdown(content, fallbackId = 'intent') {
|
|
|
17117
17612
|
: data.verification && typeof data.verification === 'object'
|
|
17118
17613
|
? data.verification
|
|
17119
17614
|
: null;
|
|
17120
|
-
|
|
17121
|
-
|
|
17122
|
-
|
|
17615
|
+
// Same precedence as every other section: body list, then frontmatter, then body prose.
|
|
17616
|
+
const verificationAll = extractVerification(sections);
|
|
17617
|
+
const verificationHasList = hasVerificationContent(extractVerification(sections, { includeProse: false }));
|
|
17618
|
+
const verification = verificationHasList
|
|
17619
|
+
? verificationAll
|
|
17620
|
+
: frontmatterVerification && hasVerificationContent(frontmatterVerification)
|
|
17621
|
+
? frontmatterVerification
|
|
17622
|
+
: hasVerificationContent(verificationAll)
|
|
17623
|
+
? verificationAll
|
|
17624
|
+
: (frontmatterVerification || {});
|
|
17123
17625
|
const version = Number(data.version);
|
|
17124
17626
|
return {
|
|
17125
17627
|
id: data.id || fallbackId,
|
|
@@ -17131,11 +17633,19 @@ function parseIntentMarkdown(content, fallbackId = 'intent') {
|
|
|
17131
17633
|
title: data.title || data.userGoal || extractTitle(body) || 'Untitled Intent',
|
|
17132
17634
|
stageName: data.stage || undefined,
|
|
17133
17635
|
severity: data.severity || undefined,
|
|
17134
|
-
outcomes: attachOutcomeMeasurements(
|
|
17636
|
+
outcomes: attachOutcomeMeasurements(preferListThenFrontmatter(sections, 'Outcomes', data.outcomes), data.outcomeMeasurements),
|
|
17135
17637
|
decisions: extractDecisions(sections),
|
|
17136
|
-
constraints:
|
|
17137
|
-
edgeCases: (() => {
|
|
17138
|
-
|
|
17638
|
+
constraints: preferListThenFrontmatter(sections, 'Constraints', data.constraints),
|
|
17639
|
+
edgeCases: (() => {
|
|
17640
|
+
const all = extractEdgeCases(sections);
|
|
17641
|
+
if (extractEdgeCases(sections, { includeProse: false }).length)
|
|
17642
|
+
return all;
|
|
17643
|
+
const fromFrontmatter = frontmatterEdgeCases(data.edgeCases);
|
|
17644
|
+
if (fromFrontmatter.length)
|
|
17645
|
+
return fromFrontmatter;
|
|
17646
|
+
return all;
|
|
17647
|
+
})(),
|
|
17648
|
+
healthMetrics: preferListThenFrontmatter(sections, 'Health Metrics', data.healthMetrics),
|
|
17139
17649
|
scope: extractScope(sections) || undefined,
|
|
17140
17650
|
verification,
|
|
17141
17651
|
confirmations: extractConfirmations(sections),
|
|
@@ -17249,17 +17759,115 @@ function stripListMarker(line) {
|
|
|
17249
17759
|
function isListItem(line) {
|
|
17250
17760
|
return /^\s*[-*]\s/.test(line);
|
|
17251
17761
|
}
|
|
17252
|
-
|
|
17253
|
-
|
|
17254
|
-
|
|
17255
|
-
|
|
17256
|
-
|
|
17762
|
+
/**
|
|
17763
|
+
* Structure and generated captions are not content; everything else under a heading is.
|
|
17764
|
+
*
|
|
17765
|
+
* Two distinct exclusions, and they are not the same rule:
|
|
17766
|
+
*
|
|
17767
|
+
* A heading line is structure. `splitSections` only opens a section on `^## `, so a `### Regression
|
|
17768
|
+
* checks` subheading falls through as a body line, and treating it as prose made the HEADING an
|
|
17769
|
+
* executable check: a `## Verification` containing nothing but subheadings passed the gate.
|
|
17770
|
+
*
|
|
17771
|
+
* A generated caption is ours, not the author's, and the registry in serializeIntentMd.ts names
|
|
17772
|
+
* exactly which strings those are. This deliberately does NOT exclude all italic prose: an author
|
|
17773
|
+
* writing `*Must not slow down page load.*` under Constraints means it, and reporting that section
|
|
17774
|
+
* as absent is the accusation the four-state gate exists to stop making.
|
|
17775
|
+
*/
|
|
17776
|
+
function isNotContent(line) {
|
|
17777
|
+
return /^#{1,6}\s/.test(line.trim()) || (0, serializeIntentMd_1.isGeneratedCaption)(line);
|
|
17778
|
+
}
|
|
17779
|
+
/**
|
|
17780
|
+
* Items in a section, preserving what the author actually wrote.
|
|
17781
|
+
*
|
|
17782
|
+
* List items keep their existing boundaries: one item per marker, with an indented
|
|
17783
|
+
* continuation line folded into the item it wraps. A run of unindented non-list lines is one
|
|
17784
|
+
* paragraph, and one paragraph is one item; blank lines separate paragraphs. Prose is never
|
|
17785
|
+
* split into sentences, so an author who writes two independently checkable outcomes as two
|
|
17786
|
+
* paragraphs gets two items and one who writes them as a single block gets one.
|
|
17787
|
+
*
|
|
17788
|
+
* Before this, a `.filter(isListItem)` dropped every prose line here and the gates then
|
|
17789
|
+
* reported the section as `absent`, which told an author who had written a constraint in prose
|
|
17790
|
+
* that they had written nothing. Preserving the text is what lets readiness say `unconfirmed`
|
|
17791
|
+
* and quote it back instead. The gates themselves are unchanged: nothing about the parse
|
|
17792
|
+
* relaxes a threshold, and a formatting choice never earns a pass it would not otherwise get.
|
|
17793
|
+
*/
|
|
17794
|
+
function extractItemsSection(sections, heading, { includeProse = true } = {}) {
|
|
17795
|
+
const items = [];
|
|
17796
|
+
let paragraph = [];
|
|
17797
|
+
let lastWasListItem = false;
|
|
17798
|
+
const flushParagraph = () => {
|
|
17799
|
+
const text = paragraph.join(' ').replace(/\s+/g, ' ').trim();
|
|
17800
|
+
if (text)
|
|
17801
|
+
items.push(text);
|
|
17802
|
+
paragraph = [];
|
|
17803
|
+
};
|
|
17804
|
+
for (const line of sectionLines(sections, heading)) {
|
|
17805
|
+
if (!line.trim()) {
|
|
17806
|
+
flushParagraph();
|
|
17807
|
+
lastWasListItem = false;
|
|
17808
|
+
continue;
|
|
17809
|
+
}
|
|
17810
|
+
// Structure and generated captions are checked FIRST, so neither can be folded into the
|
|
17811
|
+
// item above it or accumulated into a paragraph. A subheading is not a wrapped bullet.
|
|
17812
|
+
if (isNotContent(line)) {
|
|
17813
|
+
flushParagraph();
|
|
17814
|
+
lastWasListItem = false;
|
|
17815
|
+
continue;
|
|
17816
|
+
}
|
|
17817
|
+
if (isListItem(line)) {
|
|
17818
|
+
flushParagraph();
|
|
17819
|
+
const text = stripListMarker(line);
|
|
17820
|
+
lastWasListItem = Boolean(text);
|
|
17821
|
+
if (text)
|
|
17822
|
+
items.push(text);
|
|
17823
|
+
continue;
|
|
17824
|
+
}
|
|
17825
|
+
// A non-blank line straight after a list item is that item's wrapped text, not a new
|
|
17826
|
+
// item. Markdown lazy continuation does not require indentation, and requiring it let a
|
|
17827
|
+
// hard-wrapped bullet split into two items: wrapping must never manufacture an outcome
|
|
17828
|
+
// that clears the two-outcome gate. A blank line is what ends an item. This runs
|
|
17829
|
+
// whatever `includeProse` says, because wrapped text belongs to the list item, not to
|
|
17830
|
+
// the prose the flag governs.
|
|
17831
|
+
if (lastWasListItem && items.length) {
|
|
17832
|
+
items[items.length - 1] = `${items[items.length - 1]} ${line.trim()}`.replace(/\s+/g, ' ');
|
|
17833
|
+
continue;
|
|
17834
|
+
}
|
|
17835
|
+
if (!includeProse) {
|
|
17836
|
+
flushParagraph();
|
|
17837
|
+
lastWasListItem = false;
|
|
17838
|
+
continue;
|
|
17839
|
+
}
|
|
17840
|
+
lastWasListItem = false;
|
|
17841
|
+
paragraph.push(line.trim());
|
|
17842
|
+
}
|
|
17843
|
+
flushParagraph();
|
|
17844
|
+
return items;
|
|
17257
17845
|
}
|
|
17258
17846
|
/**
|
|
17259
17847
|
* Parse `## Decisions & Ruled-Out Alternatives` entries written by `decisionLines()`:
|
|
17260
17848
|
* - **choice** (instead of: ruledOut) — reason
|
|
17261
17849
|
* The separator is an em dash when written by us; a plain hyphen is accepted for hand-edited files.
|
|
17262
17850
|
*/
|
|
17851
|
+
/**
|
|
17852
|
+
* A section that has list items owns the field, prose included. A section that is prose only
|
|
17853
|
+
* yields to frontmatter, and is read only when there is no frontmatter to yield to.
|
|
17854
|
+
*
|
|
17855
|
+
* The middle step is the published normalization rule, conformance case "list fallback: a
|
|
17856
|
+
* section with no list items does not override frontmatter": prose under a heading must not
|
|
17857
|
+
* erase a structured frontmatter value. It is a rule about OVERRIDE, not about ignoring prose,
|
|
17858
|
+
* so a section mixing a bullet and a paragraph keeps both. The last step is the defect this
|
|
17859
|
+
* change exists for: with nothing in frontmatter either, prose used to vanish and the gate
|
|
17860
|
+
* reported the section as `absent`, telling the author they had written nothing.
|
|
17861
|
+
*/
|
|
17862
|
+
function preferListThenFrontmatter(sections, heading, fromFrontmatter) {
|
|
17863
|
+
const all = extractItemsSection(sections, heading);
|
|
17864
|
+
if (extractItemsSection(sections, heading, { includeProse: false }).length)
|
|
17865
|
+
return all;
|
|
17866
|
+
const frontmatter = preferSection([], fromFrontmatter);
|
|
17867
|
+
if (frontmatter.length)
|
|
17868
|
+
return frontmatter;
|
|
17869
|
+
return all;
|
|
17870
|
+
}
|
|
17263
17871
|
function extractDecisions(sections) {
|
|
17264
17872
|
const out = [];
|
|
17265
17873
|
for (const line of sectionLines(sections, DECISIONS_HEADING)) {
|
|
@@ -17278,12 +17886,11 @@ function extractDecisions(sections) {
|
|
|
17278
17886
|
}
|
|
17279
17887
|
return out;
|
|
17280
17888
|
}
|
|
17281
|
-
function extractEdgeCases(sections) {
|
|
17889
|
+
function extractEdgeCases(sections, opts = {}) {
|
|
17282
17890
|
const cases = [];
|
|
17283
|
-
|
|
17284
|
-
|
|
17285
|
-
|
|
17286
|
-
const clean = stripListMarker(line);
|
|
17891
|
+
// Items come from the shared reader, so a prose paragraph is an item exactly as a bullet is
|
|
17892
|
+
// and the pair patterns below are applied to both. Nothing here depends on the formatting.
|
|
17893
|
+
for (const clean of extractItemsSection(sections, 'Edge Cases', opts)) {
|
|
17287
17894
|
// Pattern: **scenario**: expected behavior
|
|
17288
17895
|
const match = clean.match(/^\*\*(.+?)\*\*:\s*(.+)$/);
|
|
17289
17896
|
if (match) {
|
|
@@ -17297,7 +17904,13 @@ function extractEdgeCases(sections) {
|
|
|
17297
17904
|
const arrowMatch = clean.match(/^(.+?)\s*(?:->|→|:)\s*(.+)$/);
|
|
17298
17905
|
if (arrowMatch) {
|
|
17299
17906
|
cases.push({ scenario: arrowMatch[1].trim(), expectedBehavior: arrowMatch[2].trim() });
|
|
17907
|
+
continue;
|
|
17300
17908
|
}
|
|
17909
|
+
// No pair in it. Keep the scenario so readiness can quote it back and ask for the
|
|
17910
|
+
// expected behavior, and leave `expectedBehavior` empty: the gate requires both, so an
|
|
17911
|
+
// unpaired item cannot pass, and no behavior is invented for the author. Dropping it,
|
|
17912
|
+
// which is what happened before, reported the section as absent instead.
|
|
17913
|
+
cases.push({ scenario: clean, expectedBehavior: '' });
|
|
17301
17914
|
}
|
|
17302
17915
|
return cases;
|
|
17303
17916
|
}
|
|
@@ -17381,7 +17994,7 @@ function parseVerificationLabel(line) {
|
|
|
17381
17994
|
* ("E2E Tests", "Unit Tests", "Manual Checks") keep their string-array buckets, which
|
|
17382
17995
|
* `toVerificationChecks()` in intent-compiler.ts adapts on read.
|
|
17383
17996
|
*/
|
|
17384
|
-
function extractVerification(sections) {
|
|
17997
|
+
function extractVerification(sections, { includeProse = true } = {}) {
|
|
17385
17998
|
const lines = sectionLines(sections, 'Verification');
|
|
17386
17999
|
if (lines.length === 0)
|
|
17387
18000
|
return {};
|
|
@@ -17389,9 +18002,55 @@ function extractVerification(sections) {
|
|
|
17389
18002
|
let currentKind = null;
|
|
17390
18003
|
let currentLegacy = null;
|
|
17391
18004
|
let sawLabel = false;
|
|
18005
|
+
// Prose is preserved here on the same terms as everywhere else: a run of unindented
|
|
18006
|
+
// non-list lines is one paragraph and one check, attributed to the bucket it was written
|
|
18007
|
+
// under, and an indented wrap folds into the check above it rather than becoming a check of
|
|
18008
|
+
// its own. A bullet parses exactly as it did before.
|
|
18009
|
+
const paragraph = [];
|
|
18010
|
+
// A holder, not a bare `let`: TypeScript narrows a variable assigned only inside
|
|
18011
|
+
// closures down to its initializer, and the call below then reads as `never`.
|
|
18012
|
+
const last = { append: null };
|
|
18013
|
+
const pushText = (text) => {
|
|
18014
|
+
if (currentKind) {
|
|
18015
|
+
const { description, status, verifies } = parseCheckLine(text);
|
|
18016
|
+
if (!description) {
|
|
18017
|
+
last.append = null;
|
|
18018
|
+
return;
|
|
18019
|
+
}
|
|
18020
|
+
const check = { kind: currentKind, description };
|
|
18021
|
+
if (status)
|
|
18022
|
+
check.status = status;
|
|
18023
|
+
if (verifies)
|
|
18024
|
+
check.verifies = verifies;
|
|
18025
|
+
(result.checks ||= []).push(check);
|
|
18026
|
+
last.append = (extra) => { check.description = `${check.description} ${extra}`.replace(/\s+/g, ' '); };
|
|
18027
|
+
return;
|
|
18028
|
+
}
|
|
18029
|
+
const bucket = currentLegacy
|
|
18030
|
+
? (result[currentLegacy] ||= [])
|
|
18031
|
+
// IntentSpec labels are optional. Treat an ungrouped item as a manual check so a
|
|
18032
|
+
// valid flat `## Verification` section survives adoption instead of disappearing.
|
|
18033
|
+
: sawLabel ? null : (result.manualChecks ||= []);
|
|
18034
|
+
if (!bucket) {
|
|
18035
|
+
last.append = null;
|
|
18036
|
+
return;
|
|
18037
|
+
}
|
|
18038
|
+
bucket.push(text);
|
|
18039
|
+
last.append = (extra) => { bucket[bucket.length - 1] = `${bucket[bucket.length - 1]} ${extra}`.replace(/\s+/g, ' '); };
|
|
18040
|
+
};
|
|
18041
|
+
const flushParagraph = () => {
|
|
18042
|
+
const text = paragraph.join(' ').replace(/\s+/g, ' ').trim();
|
|
18043
|
+
paragraph.length = 0;
|
|
18044
|
+
if (text)
|
|
18045
|
+
pushText(text);
|
|
18046
|
+
};
|
|
17392
18047
|
for (const line of lines) {
|
|
17393
18048
|
const label = parseVerificationLabel(line);
|
|
17394
18049
|
if (label) {
|
|
18050
|
+
// Flush before switching buckets so the paragraph lands under the label it was
|
|
18051
|
+
// written beneath, not the next one.
|
|
18052
|
+
flushParagraph();
|
|
18053
|
+
last.append = null;
|
|
17395
18054
|
sawLabel = true;
|
|
17396
18055
|
const kind = VERIFICATION_LABEL_TO_KIND[label.toLowerCase()];
|
|
17397
18056
|
if (kind) {
|
|
@@ -17412,31 +18071,41 @@ function extractVerification(sections) {
|
|
|
17412
18071
|
currentLegacy = null;
|
|
17413
18072
|
continue;
|
|
17414
18073
|
}
|
|
17415
|
-
if (!
|
|
18074
|
+
if (!line.trim()) {
|
|
18075
|
+
flushParagraph();
|
|
18076
|
+
last.append = null;
|
|
17416
18077
|
continue;
|
|
17417
|
-
|
|
17418
|
-
|
|
18078
|
+
}
|
|
18079
|
+
// Structure first, so a `### Regression checks` subheading under `## Verification` can
|
|
18080
|
+
// neither become a check of its own nor be folded into the check above it. It used to
|
|
18081
|
+
// become one, and a Verification section holding nothing but subheadings passed.
|
|
18082
|
+
if (isNotContent(line)) {
|
|
18083
|
+
flushParagraph();
|
|
18084
|
+
last.append = null;
|
|
17419
18085
|
continue;
|
|
17420
|
-
|
|
17421
|
-
|
|
17422
|
-
|
|
18086
|
+
}
|
|
18087
|
+
if (isListItem(line)) {
|
|
18088
|
+
flushParagraph();
|
|
18089
|
+
const text = stripListMarker(line);
|
|
18090
|
+
if (!text) {
|
|
18091
|
+
last.append = null;
|
|
17423
18092
|
continue;
|
|
17424
|
-
|
|
17425
|
-
|
|
17426
|
-
|
|
17427
|
-
if (verifies)
|
|
17428
|
-
check.verifies = verifies;
|
|
17429
|
-
(result.checks ||= []).push(check);
|
|
18093
|
+
}
|
|
18094
|
+
pushText(text);
|
|
18095
|
+
continue;
|
|
17430
18096
|
}
|
|
17431
|
-
|
|
17432
|
-
|
|
18097
|
+
// Lazy continuation, as above: a wrapped check is one check.
|
|
18098
|
+
if (last.append) {
|
|
18099
|
+
last.append(line.trim());
|
|
18100
|
+
continue;
|
|
17433
18101
|
}
|
|
17434
|
-
|
|
17435
|
-
|
|
17436
|
-
|
|
17437
|
-
(result.manualChecks ||= []).push(text);
|
|
18102
|
+
if (!includeProse) {
|
|
18103
|
+
flushParagraph();
|
|
18104
|
+
continue;
|
|
17438
18105
|
}
|
|
18106
|
+
paragraph.push(line.trim());
|
|
17439
18107
|
}
|
|
18108
|
+
flushParagraph();
|
|
17440
18109
|
return result;
|
|
17441
18110
|
}
|
|
17442
18111
|
function hasVerificationContent(v) {
|
|
@@ -17753,7 +18422,7 @@ function readOpenSpecChangeForPathmode(ref, io = {}) {
|
|
|
17753
18422
|
* https://preflight.pathmode.io, so keep it true.
|
|
17754
18423
|
*/
|
|
17755
18424
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
17756
|
-
exports.READINESS_GATE_ORDER = exports.READINESS_BLOCKER_DESCRIPTIONS = void 0;
|
|
18425
|
+
exports.READINESS_GATE_ORDER = exports.READINESS_OUTCOMES_TOO_FEW_BLOCKER = exports.READINESS_BLOCKER_DESCRIPTIONS = void 0;
|
|
17757
18426
|
exports.coerceText = coerceText;
|
|
17758
18427
|
exports.asArray = asArray;
|
|
17759
18428
|
exports.isTitleMeaningful = isTitleMeaningful;
|
|
@@ -17761,6 +18430,7 @@ exports.isConstraintConcrete = isConstraintConcrete;
|
|
|
17761
18430
|
exports.isVerificationCheckSubstantive = isVerificationCheckSubstantive;
|
|
17762
18431
|
exports.isObjectiveSpecific = isObjectiveSpecific;
|
|
17763
18432
|
exports.isOutcomeMeasurable = isOutcomeMeasurable;
|
|
18433
|
+
exports.readinessBlockerFor = readinessBlockerFor;
|
|
17764
18434
|
exports.normalizeAnchor = normalizeAnchor;
|
|
17765
18435
|
exports.fieldDigest = fieldDigest;
|
|
17766
18436
|
exports.computeReadinessVerdict = computeReadinessVerdict;
|
|
@@ -17880,6 +18550,18 @@ exports.READINESS_BLOCKER_DESCRIPTIONS = {
|
|
|
17880
18550
|
edgeCases: 'No edge case with a defined expected behavior.',
|
|
17881
18551
|
verification: 'No concrete verification — describe at least one check specific enough to run.',
|
|
17882
18552
|
};
|
|
18553
|
+
/**
|
|
18554
|
+
* MIRROR of `OUTCOMES_TOO_FEW_BLOCKER` in lib/intentReadiness.ts; see its comment for why the
|
|
18555
|
+
* outcomes gate needs two messages. The threshold is unchanged.
|
|
18556
|
+
*/
|
|
18557
|
+
exports.READINESS_OUTCOMES_TOO_FEW_BLOCKER = 'Fewer than two outcomes. Name at least two, one per line or paragraph, so each can be checked on its own.';
|
|
18558
|
+
function readinessBlockerFor(key, spec) {
|
|
18559
|
+
if (key !== 'outcomes')
|
|
18560
|
+
return exports.READINESS_BLOCKER_DESCRIPTIONS[key];
|
|
18561
|
+
return asArray(spec.outcomes).map(o => outcomeText(o).trim()).filter(Boolean).length < 2
|
|
18562
|
+
? exports.READINESS_OUTCOMES_TOO_FEW_BLOCKER
|
|
18563
|
+
: exports.READINESS_BLOCKER_DESCRIPTIONS.outcomes;
|
|
18564
|
+
}
|
|
17883
18565
|
/** Display order for the gate strip (title first, matching the Preflight page). */
|
|
17884
18566
|
// ── Confirmation anchoring ──────────────────────────────────────────────────
|
|
17885
18567
|
/**
|
|
@@ -18059,7 +18741,7 @@ function computeReadinessVerdict(spec) {
|
|
|
18059
18741
|
};
|
|
18060
18742
|
const failingBlockers = BLOCKER_ORDER
|
|
18061
18743
|
.filter(k => !signals[k])
|
|
18062
|
-
.map(k =>
|
|
18744
|
+
.map(k => readinessBlockerFor(k, spec));
|
|
18063
18745
|
// What the reader read, per dimension, independent of whether the heuristic accepted it.
|
|
18064
18746
|
// Placeholder text is deliberately not extracted: no path from a blank spec to confirmable.
|
|
18065
18747
|
const objectiveText = coerceText(spec.objective).trim();
|
|
@@ -18077,11 +18759,14 @@ function computeReadinessVerdict(spec) {
|
|
|
18077
18759
|
const detail = {};
|
|
18078
18760
|
for (const key of exports.READINESS_GATE_ORDER) {
|
|
18079
18761
|
const text = extractedBy[key] ?? '';
|
|
18762
|
+
// Only when it OVERRIDES the static message, so the common shape is untouched.
|
|
18763
|
+
const resolved = readinessBlockerFor(key, spec);
|
|
18764
|
+
const override = resolved && resolved !== exports.READINESS_BLOCKER_DESCRIPTIONS[key] ? { blocker: resolved } : {};
|
|
18080
18765
|
detail[key] = signals[key]
|
|
18081
18766
|
? { state: 'pass', ...(text ? { extracted: text } : {}) }
|
|
18082
18767
|
: text
|
|
18083
|
-
? { state: 'unconfirmed', extracted: text }
|
|
18084
|
-
: { state: 'absent' };
|
|
18768
|
+
? { state: 'unconfirmed', extracted: text, ...override }
|
|
18769
|
+
: { state: 'absent', ...override };
|
|
18085
18770
|
}
|
|
18086
18771
|
// Confirmations only ever move a dimension the heuristics already failed; `signals` is never
|
|
18087
18772
|
// mutated, so the lexical verdict stays visible underneath.
|
|
@@ -18139,15 +18824,36 @@ function formatReadinessFrontmatter(verdict) {
|
|
|
18139
18824
|
* The extra line an `unconfirmed` dimension earns: what was read, and an honest statement of
|
|
18140
18825
|
* what the check could not find in it.
|
|
18141
18826
|
*
|
|
18142
|
-
*
|
|
18143
|
-
* audit measured that most failures on those two are false negatives from a fixed English
|
|
18144
|
-
* vocabulary
|
|
18145
|
-
*
|
|
18827
|
+
* Objective and outcomes get the SOFTENED copy, and that scoping is the finding, not caution:
|
|
18828
|
+
* the audit measured that most failures on those two are false negatives from a fixed English
|
|
18829
|
+
* vocabulary, so telling those authors the text may be fine is honest.
|
|
18830
|
+
*
|
|
18831
|
+
* Constraints, edge cases and verification get a hint too, but a plainer one. Their measured
|
|
18832
|
+
* misses were extraction failures, and the audit never measured their lexical recall, so the
|
|
18833
|
+
* copy states what was read and what is missing from it and stops there. It does not say the
|
|
18834
|
+
* check is probably wrong, because for these three there is no evidence that it is. They earn
|
|
18835
|
+
* a hint at all only because the parser now preserves prose: before that, an unread section
|
|
18836
|
+
* reached this code as `absent`, there was nothing extracted to quote, and the reader was told
|
|
18837
|
+
* it had written nothing.
|
|
18146
18838
|
*/
|
|
18147
18839
|
const UNCONFIRMED_HINT = {
|
|
18148
18840
|
objective: 'Read this, but could not confirm an affected actor and a concrete problem in it. If it already names them, this check reads a fixed vocabulary and may be missing valid phrasing:',
|
|
18149
18841
|
outcomes: 'Read these, but could not confirm an observable threshold in enough of them. If they are already observable, this check reads a fixed vocabulary and may be missing valid phrasing:',
|
|
18842
|
+
constraints: 'Read this, but could not confirm a hard limit in it. A constraint names something that must never happen:',
|
|
18843
|
+
edgeCases: 'Read this, but no expected behavior was paired with it. Write the case as "situation: what should happen", one per line or paragraph:',
|
|
18844
|
+
verification: 'Read this, but could not confirm a check specific enough to run. A check names a command or a stated expected result:',
|
|
18150
18845
|
};
|
|
18846
|
+
/**
|
|
18847
|
+
* The hint has to follow the blocker that was actually chosen. When outcomes failed on COUNT,
|
|
18848
|
+
* the measurability hint contradicted the line directly above it and accused wording the
|
|
18849
|
+
* detector had accepted.
|
|
18850
|
+
*/
|
|
18851
|
+
function unconfirmedHintFor(key, d) {
|
|
18852
|
+
if (key === 'outcomes' && d?.blocker === exports.READINESS_OUTCOMES_TOO_FEW_BLOCKER) {
|
|
18853
|
+
return 'Read this. If it holds more than one outcome, separate them so each can be checked on its own:';
|
|
18854
|
+
}
|
|
18855
|
+
return UNCONFIRMED_HINT[key];
|
|
18856
|
+
}
|
|
18151
18857
|
/** Keep a quoted extraction to one readable line; the full text is in verdict.detail. */
|
|
18152
18858
|
function truncateForQuote(text, max = 160) {
|
|
18153
18859
|
const flat = text.replace(/\s+/g, ' ').trim();
|
|
@@ -18199,10 +18905,11 @@ repairHint) {
|
|
|
18199
18905
|
for (const key of BLOCKER_ORDER) {
|
|
18200
18906
|
if (verdict.signals[key] || resolved(key) === 'pass' || resolved(key) === 'not_applicable')
|
|
18201
18907
|
continue;
|
|
18202
|
-
lines.push(` ✗ ${exports.READINESS_BLOCKER_DESCRIPTIONS[key]}`);
|
|
18908
|
+
lines.push(` ✗ ${verdict.detail?.[key]?.blocker ?? exports.READINESS_BLOCKER_DESCRIPTIONS[key]}`);
|
|
18203
18909
|
const d = verdict.detail?.[key];
|
|
18204
|
-
|
|
18205
|
-
|
|
18910
|
+
const hint = unconfirmedHintFor(key, d);
|
|
18911
|
+
if (d?.state === 'unconfirmed' && d.extracted && hint) {
|
|
18912
|
+
lines.push(` ↳ ${hint}`);
|
|
18206
18913
|
lines.push(` "${truncateForQuote(d.extracted)}"`);
|
|
18207
18914
|
}
|
|
18208
18915
|
}
|
|
@@ -29964,7 +30671,7 @@ module.exports = /*#__PURE__*/JSON.parse('{"$schema":"http://json-schema.org/dra
|
|
|
29964
30671
|
/***/ ((module) => {
|
|
29965
30672
|
|
|
29966
30673
|
"use strict";
|
|
29967
|
-
module.exports = {"rE":"2.
|
|
30674
|
+
module.exports = {"rE":"2.2.0"};
|
|
29968
30675
|
|
|
29969
30676
|
/***/ })
|
|
29970
30677
|
|