@pathmode/cli 2.1.1 → 2.1.2
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 +199 -5
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -16379,7 +16379,11 @@ const isGitHubAction = (!!process.env.GITHUB_ACTIONS || !!process.env.INPUT_FILE
|
|
|
16379
16379
|
program
|
|
16380
16380
|
.name('pathmode')
|
|
16381
16381
|
.description('Pathmode CLI — Intent Engineering for AI product teams')
|
|
16382
|
-
.
|
|
16382
|
+
// Read from package.json rather than a literal: the two drifted once already (package.json
|
|
16383
|
+
// said 2.1.2 while `--version` still answered 2.1.1), and a CLI that misreports its own
|
|
16384
|
+
// version makes every "which build are you on?" bug report unanswerable. ncc inlines the
|
|
16385
|
+
// JSON at build time, so this stays a single self-contained bundle.
|
|
16386
|
+
.version((__nccwpck_require__(8330)/* .version */ .rE));
|
|
16383
16387
|
// ============================================================
|
|
16384
16388
|
// validate — Existing command (preserved)
|
|
16385
16389
|
// ============================================================
|
|
@@ -16435,6 +16439,7 @@ program
|
|
|
16435
16439
|
const readiness_1 = __nccwpck_require__(2804);
|
|
16436
16440
|
const local_reader_1 = __nccwpck_require__(8963);
|
|
16437
16441
|
const openspec_reader_1 = __nccwpck_require__(7290);
|
|
16442
|
+
const openspec_product_reader_1 = __nccwpck_require__(1615);
|
|
16438
16443
|
/**
|
|
16439
16444
|
* Resolve what to grade, in the order a person would expect: an explicit path wins, then a local
|
|
16440
16445
|
* intent.md, then the OpenSpec change this repo is already working on. The last one is the point:
|
|
@@ -16446,8 +16451,10 @@ function resolvePreflightSubject(target) {
|
|
|
16446
16451
|
if (!fs_1.default.existsSync(abs))
|
|
16447
16452
|
return { error: `Nothing at ${abs}` };
|
|
16448
16453
|
if (fs_1.default.statSync(abs).isDirectory()) {
|
|
16449
|
-
const
|
|
16450
|
-
|
|
16454
|
+
const read = (0, openspec_product_reader_1.readOpenSpecChangeForPathmode)({ id: path_1.default.basename(abs), dir: abs });
|
|
16455
|
+
if ((0, openspec_product_reader_1.isProductReadError)(read))
|
|
16456
|
+
return { error: read.error };
|
|
16457
|
+
return { spec: read.spec, source: abs, kind: 'openspec', notes: read.notes };
|
|
16451
16458
|
}
|
|
16452
16459
|
return { spec: (0, local_reader_1.parseIntentMarkdown)(fs_1.default.readFileSync(abs, 'utf-8')), source: abs, kind: 'intent.md' };
|
|
16453
16460
|
}
|
|
@@ -16458,7 +16465,10 @@ function resolvePreflightSubject(target) {
|
|
|
16458
16465
|
const ctx = (0, openspec_reader_1.detectOpenSpec)(process.cwd());
|
|
16459
16466
|
if (ctx && ctx.changes.length === 1) {
|
|
16460
16467
|
const change = ctx.changes[0];
|
|
16461
|
-
|
|
16468
|
+
const read = (0, openspec_product_reader_1.readOpenSpecChangeForPathmode)(change);
|
|
16469
|
+
if ((0, openspec_product_reader_1.isProductReadError)(read))
|
|
16470
|
+
return { error: read.error };
|
|
16471
|
+
return { spec: read.spec, source: change.dir, kind: 'openspec', notes: read.notes };
|
|
16462
16472
|
}
|
|
16463
16473
|
if (ctx && ctx.changes.length > 1) {
|
|
16464
16474
|
// Same rule the MCP resolver documents: never guess between changes. Directory order is
|
|
@@ -16476,13 +16486,17 @@ const preflight = async (target) => {
|
|
|
16476
16486
|
console.error(chalk_1.default.red(`✗ ${resolved.error}`));
|
|
16477
16487
|
process.exit(2);
|
|
16478
16488
|
}
|
|
16479
|
-
const { spec, source, kind } = resolved;
|
|
16489
|
+
const { spec, source, kind, notes } = resolved;
|
|
16480
16490
|
const verdict = (0, readiness_1.computeReadinessVerdict)(spec);
|
|
16481
16491
|
// A relative path that climbs out of cwd is noise; show the real one.
|
|
16482
16492
|
const relCandidate = path_1.default.relative(process.cwd(), source);
|
|
16483
16493
|
const rel = !relCandidate || relCandidate.startsWith('..') ? source : relCandidate;
|
|
16484
16494
|
console.log();
|
|
16485
16495
|
console.log(chalk_1.default.dim(`Read ${kind === 'openspec' ? 'OpenSpec change' : 'intent.md'} at ${rel}`));
|
|
16496
|
+
// Which document the verdict is actually about. Without this, a change carrying both an
|
|
16497
|
+
// intent.md and a proposal.md gives no clue which one was graded.
|
|
16498
|
+
for (const note of notes ?? [])
|
|
16499
|
+
console.log(chalk_1.default.dim(` ${note}`));
|
|
16486
16500
|
console.log();
|
|
16487
16501
|
// Show what was extracted per dimension BEFORE the verdict, so a wrong verdict is visibly
|
|
16488
16502
|
// traceable to a wrong read rather than looking like a judgment about the author.
|
|
@@ -17330,6 +17344,178 @@ function hasVerificationContent(v) {
|
|
|
17330
17344
|
}
|
|
17331
17345
|
|
|
17332
17346
|
|
|
17347
|
+
/***/ }),
|
|
17348
|
+
|
|
17349
|
+
/***/ 1615:
|
|
17350
|
+
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
|
17351
|
+
|
|
17352
|
+
"use strict";
|
|
17353
|
+
|
|
17354
|
+
/**
|
|
17355
|
+
* The product-judgment adapter for an OpenSpec change.
|
|
17356
|
+
*
|
|
17357
|
+
* WHY THIS IS A SEPARATE LAYER. `scripts/openspec-corpus/openspec-reader.ts` is a frozen
|
|
17358
|
+
* instrument: the corpus measurements in docs/research/openspec-corpus-audit.md are only
|
|
17359
|
+
* reproducible while it keeps reading exactly what it read then (that is also why it derives the
|
|
17360
|
+
* title from the directory name rather than an H1, and why `openspec-import.ts` layers the nicer
|
|
17361
|
+
* title on top instead of editing it). Teaching that file about `intent.md` would silently
|
|
17362
|
+
* invalidate every recorded rate. So preference lives here, above it, and the reader stays
|
|
17363
|
+
* untouched.
|
|
17364
|
+
*
|
|
17365
|
+
* WHAT THIS FIXES. The `pathmode-intent` schema writes the product judgment to a change-local
|
|
17366
|
+
* `intent.md`, ahead of the proposal. The corpus reader has never heard of that file, so before
|
|
17367
|
+
* this adapter existed, a change authored faithfully through our own schema graded as
|
|
17368
|
+
* "nothing found" on five of six dimensions: we would have shipped a schema whose output our own
|
|
17369
|
+
* gate accused of having no product judgment. That is the false-accusation failure the whole
|
|
17370
|
+
* unconfirmed-not-absent program exists to remove, so it must not be reintroduced by two of our
|
|
17371
|
+
* own tools disagreeing about where judgment lives.
|
|
17372
|
+
*
|
|
17373
|
+
* THE PRECEDENCE RULES, and each one is a decision rather than a default:
|
|
17374
|
+
*
|
|
17375
|
+
* 1. A recognized change-local `intent.md` is AUTHORITATIVE for the product fields. It was
|
|
17376
|
+
* written to answer exactly these questions, in a format built for them.
|
|
17377
|
+
* 2. No `intent.md` -> fall back to `openSpecChangeToSpec()` unchanged, so a standard OpenSpec
|
|
17378
|
+
* repo grades exactly as it did before this file existed. Pinned by test.
|
|
17379
|
+
* 3. An `intent.md` that exists but cannot be read is an ERROR, never a silent fallback. Quietly
|
|
17380
|
+
* grading proposal.md instead would report a verdict about a document the author did not
|
|
17381
|
+
* write, and hide the broken file that caused it.
|
|
17382
|
+
* 4. Blank fields are NEVER backfilled from proposal.md. A missing objective in an intent.md is
|
|
17383
|
+
* an unfinished product judgment, and the gate saying so is the product working. Merging the
|
|
17384
|
+
* proposal's `## Why` over the gap would manufacture a pass out of a document written to
|
|
17385
|
+
* answer a different question.
|
|
17386
|
+
*
|
|
17387
|
+
* Read-only throughout: nothing here writes, moves, or creates a file.
|
|
17388
|
+
*/
|
|
17389
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17390
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
17391
|
+
};
|
|
17392
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
17393
|
+
exports.CHANGE_LOCAL_INTENT_FILE = void 0;
|
|
17394
|
+
exports.isProductReadError = isProductReadError;
|
|
17395
|
+
exports.readOpenSpecChangeForPathmode = readOpenSpecChangeForPathmode;
|
|
17396
|
+
const fs_1 = __importDefault(__nccwpck_require__(9896));
|
|
17397
|
+
const path_1 = __importDefault(__nccwpck_require__(6928));
|
|
17398
|
+
const openspec_reader_1 = __nccwpck_require__(7290);
|
|
17399
|
+
const local_reader_1 = __nccwpck_require__(8963);
|
|
17400
|
+
/** The file the `pathmode-intent` schema's `product-intent` artifact generates. */
|
|
17401
|
+
exports.CHANGE_LOCAL_INTENT_FILE = 'intent.md';
|
|
17402
|
+
function isProductReadError(r) {
|
|
17403
|
+
return r.error !== undefined;
|
|
17404
|
+
}
|
|
17405
|
+
/**
|
|
17406
|
+
* Does a verification object hold anything?
|
|
17407
|
+
*
|
|
17408
|
+
* A deliberately tiny local predicate rather than `toVerificationChecks` from the intent
|
|
17409
|
+
* compiler: importing that pulled roughly a thousand generated lines of the compiler into the
|
|
17410
|
+
* published CLI bundle, and nothing here needs to normalize checks. The verification object
|
|
17411
|
+
* travels verbatim, so all this has to answer is "is there content".
|
|
17412
|
+
*/
|
|
17413
|
+
function hasVerificationContent(v) {
|
|
17414
|
+
if (!v || typeof v !== 'object')
|
|
17415
|
+
return false;
|
|
17416
|
+
const src = v;
|
|
17417
|
+
return Boolean(src.checks?.length || src.manualChecks?.length || src.unitTests?.length || src.e2eTests?.length);
|
|
17418
|
+
}
|
|
17419
|
+
/**
|
|
17420
|
+
* Does this parse carry any product judgment at all?
|
|
17421
|
+
*
|
|
17422
|
+
* Title alone does not count: `parseIntentMarkdown` defaults it to 'Untitled Intent', so a file
|
|
17423
|
+
* of pure comments would otherwise read as "recognized" and grade as five empty gates.
|
|
17424
|
+
*/
|
|
17425
|
+
function carriesJudgment(intent) {
|
|
17426
|
+
return Boolean((intent.objective || '').trim() ||
|
|
17427
|
+
(intent.outcomes || []).length ||
|
|
17428
|
+
(intent.constraints || []).length ||
|
|
17429
|
+
(intent.edgeCases || []).length ||
|
|
17430
|
+
hasVerificationContent(intent.verification));
|
|
17431
|
+
}
|
|
17432
|
+
/** Project a parsed intent.md onto the six-gate shape, keeping what makes it an intent.md. */
|
|
17433
|
+
function intentToProjection(intent) {
|
|
17434
|
+
const spec = {
|
|
17435
|
+
// The title is taken as written, with NO fallback to the change-directory name. An
|
|
17436
|
+
// earlier version substituted the directory name whenever it saw the 'Untitled Intent'
|
|
17437
|
+
// placeholder, which quietly handed a passing title gate to a scaffold nobody had filled
|
|
17438
|
+
// in: the directory is always named something, so the gate could never report an
|
|
17439
|
+
// unnamed change. `parseIntentMarkdown` already defaults a title-less file to 'Untitled
|
|
17440
|
+
// Intent', and the gate knows that string is a placeholder, so passing it through is
|
|
17441
|
+
// what makes an unfinished file read as unfinished.
|
|
17442
|
+
title: intent.title || '',
|
|
17443
|
+
objective: (intent.objective || '').trim(),
|
|
17444
|
+
outcomes: (intent.outcomes || []).map(o => (typeof o === 'string' ? o : String(o?.text ?? ''))).filter(Boolean),
|
|
17445
|
+
constraints: (intent.constraints || []).map(c => (typeof c === 'string' ? c : String(c ?? ''))).filter(Boolean),
|
|
17446
|
+
edgeCases: (intent.edgeCases || [])
|
|
17447
|
+
.map(ec => ({ scenario: String(ec?.scenario ?? '').trim(), expectedBehavior: String(ec?.expectedBehavior ?? '').trim() }))
|
|
17448
|
+
.filter(ec => ec.scenario || ec.expectedBehavior),
|
|
17449
|
+
// Verbatim: kinds are semantics, not decoration.
|
|
17450
|
+
verification: intent.verification ?? {},
|
|
17451
|
+
hasContent: true,
|
|
17452
|
+
};
|
|
17453
|
+
// Only set the key when the file actually carried one, so a spec with no Confirmations
|
|
17454
|
+
// section is indistinguishable from one that never had the concept.
|
|
17455
|
+
if (intent.confirmations?.length)
|
|
17456
|
+
spec.confirmations = intent.confirmations;
|
|
17457
|
+
return spec;
|
|
17458
|
+
}
|
|
17459
|
+
/**
|
|
17460
|
+
* Read a change's product judgment, preferring a change-local intent.md.
|
|
17461
|
+
*
|
|
17462
|
+
* `readFile` is injectable so tests can simulate an unreadable file (a permissions error, a
|
|
17463
|
+
* directory where a file belongs) without depending on the filesystem to misbehave on cue.
|
|
17464
|
+
*/
|
|
17465
|
+
function readOpenSpecChangeForPathmode(ref, io = {}) {
|
|
17466
|
+
const exists = io.exists ?? ((p) => { try {
|
|
17467
|
+
return fs_1.default.existsSync(p);
|
|
17468
|
+
}
|
|
17469
|
+
catch {
|
|
17470
|
+
return false;
|
|
17471
|
+
} });
|
|
17472
|
+
const readFile = io.readFile ?? ((p) => fs_1.default.readFileSync(p, 'utf-8'));
|
|
17473
|
+
const intentPath = path_1.default.join(ref.dir, exports.CHANGE_LOCAL_INTENT_FILE);
|
|
17474
|
+
if (!exists(intentPath)) {
|
|
17475
|
+
// Rule 2: a standard OpenSpec repo is graded exactly as before.
|
|
17476
|
+
return { spec: (0, openspec_reader_1.openSpecChangeToSpec)(ref), source: 'openspec-artifacts', notes: [] };
|
|
17477
|
+
}
|
|
17478
|
+
let raw;
|
|
17479
|
+
try {
|
|
17480
|
+
raw = readFile(intentPath);
|
|
17481
|
+
}
|
|
17482
|
+
catch (e) {
|
|
17483
|
+
// Rule 3. Name the file and the reason; do not grade something else instead.
|
|
17484
|
+
return { error: `Found ${exports.CHANGE_LOCAL_INTENT_FILE} in this change but could not read it: ${e instanceof Error ? e.message : String(e)}`, intentPath };
|
|
17485
|
+
}
|
|
17486
|
+
let intent;
|
|
17487
|
+
try {
|
|
17488
|
+
intent = (0, local_reader_1.parseIntentMarkdown)(raw, ref.id);
|
|
17489
|
+
}
|
|
17490
|
+
catch (e) {
|
|
17491
|
+
// gray-matter throws on malformed frontmatter. Same rule: an explicit failure beats a
|
|
17492
|
+
// verdict about a different document.
|
|
17493
|
+
return { error: `Found ${exports.CHANGE_LOCAL_INTENT_FILE} in this change but could not parse it: ${e instanceof Error ? e.message : String(e)}`, intentPath };
|
|
17494
|
+
}
|
|
17495
|
+
if (!carriesJudgment(intent)) {
|
|
17496
|
+
return {
|
|
17497
|
+
error: `Found ${exports.CHANGE_LOCAL_INTENT_FILE} in this change but read no product judgment from it. Expected at least one of ## Objective, ## Outcomes, ## Constraints, ## Edge Cases, or ## Verification. Renaming a heading makes its section invisible.`,
|
|
17498
|
+
intentPath,
|
|
17499
|
+
};
|
|
17500
|
+
}
|
|
17501
|
+
const notes = [
|
|
17502
|
+
`Product judgment read from ${exports.CHANGE_LOCAL_INTENT_FILE}; proposal.md, design.md, spec deltas, and tasks.md were not merged into these fields.`,
|
|
17503
|
+
];
|
|
17504
|
+
// Rule 4, said out loud rather than inferred from an empty verdict line.
|
|
17505
|
+
const blank = [
|
|
17506
|
+
!(intent.objective || '').trim() && 'objective',
|
|
17507
|
+
!(intent.outcomes || []).length && 'outcomes',
|
|
17508
|
+
!(intent.constraints || []).length && 'constraints',
|
|
17509
|
+
!(intent.edgeCases || []).length && 'edge cases',
|
|
17510
|
+
!hasVerificationContent(intent.verification) && 'verification',
|
|
17511
|
+
].filter(Boolean);
|
|
17512
|
+
if (blank.length) {
|
|
17513
|
+
notes.push(`${exports.CHANGE_LOCAL_INTENT_FILE} left ${blank.join(', ')} empty. These are reported as missing rather than filled in from proposal.md, because an unfinished product judgment is the thing worth seeing.`);
|
|
17514
|
+
}
|
|
17515
|
+
return { spec: intentToProjection(intent), source: 'intent.md', intentPath, notes };
|
|
17516
|
+
}
|
|
17517
|
+
|
|
17518
|
+
|
|
17333
17519
|
/***/ }),
|
|
17334
17520
|
|
|
17335
17521
|
/***/ 2804:
|
|
@@ -24428,6 +24614,14 @@ module.exports = /*#__PURE__*/JSON.parse('{"$id":"https://raw.githubusercontent.
|
|
|
24428
24614
|
"use strict";
|
|
24429
24615
|
module.exports = /*#__PURE__*/JSON.parse('{"$schema":"http://json-schema.org/draft-07/schema#","$id":"http://json-schema.org/draft-07/schema#","title":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"allOf":[{"$ref":"#/definitions/nonNegativeInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}},"type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference"},"$schema":{"type":"string","format":"uri"},"$ref":{"type":"string","format":"uri-reference"},"$comment":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"default":true,"readOnly":{"type":"boolean","default":false},"examples":{"type":"array","items":true},"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/definitions/nonNegativeInteger"},"minLength":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"$ref":"#"},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":true},"maxItems":{"$ref":"#/definitions/nonNegativeInteger"},"minItems":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"contains":{"$ref":"#"},"maxProperties":{"$ref":"#/definitions/nonNegativeInteger"},"minProperties":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"$ref":"#"},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"propertyNames":{"format":"regex"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"propertyNames":{"$ref":"#"},"const":true,"enum":{"type":"array","items":true,"minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"format":{"type":"string"},"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"if":{"$ref":"#"},"then":{"$ref":"#"},"else":{"$ref":"#"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"default":true}');
|
|
24430
24616
|
|
|
24617
|
+
/***/ }),
|
|
24618
|
+
|
|
24619
|
+
/***/ 8330:
|
|
24620
|
+
/***/ ((module) => {
|
|
24621
|
+
|
|
24622
|
+
"use strict";
|
|
24623
|
+
module.exports = {"rE":"2.1.2"};
|
|
24624
|
+
|
|
24431
24625
|
/***/ })
|
|
24432
24626
|
|
|
24433
24627
|
/******/ });
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pathmode/cli",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"description": "The Intent Layer CLI
|
|
7
|
+
"description": "The Intent Layer CLI — Spec-Driven Development with Pathmode",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"bin": {
|
|
10
10
|
"pathmode": "dist/index.js"
|