@pathmode/cli 2.1.1 → 2.1.3

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.
Files changed (2) hide show
  1. package/dist/index.js +223 -6
  2. 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
- .version('2.1.1');
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 spec = (0, openspec_reader_1.openSpecChangeToSpec)({ id: path_1.default.basename(abs), dir: abs });
16450
- return { spec, source: abs, kind: 'openspec' };
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
- return { spec: (0, openspec_reader_1.openSpecChangeToSpec)(change), source: change.dir, kind: 'openspec' };
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.
@@ -16870,6 +16884,7 @@ exports.readLocalIntents = readLocalIntents;
16870
16884
  exports.readIntentMeta = readIntentMeta;
16871
16885
  exports.readIntentFile = readIntentFile;
16872
16886
  exports.parseIntentMarkdown = parseIntentMarkdown;
16887
+ exports.stripHtmlComments = stripHtmlComments;
16873
16888
  const fs_1 = __importDefault(__nccwpck_require__(9896));
16874
16889
  const path_1 = __importDefault(__nccwpck_require__(6928));
16875
16890
  const gray_matter_1 = __importDefault(__nccwpck_require__(2702));
@@ -17025,7 +17040,7 @@ function frontmatterEdgeCases(v) {
17025
17040
  }
17026
17041
  function parseIntentMarkdown(content, fallbackId = 'intent') {
17027
17042
  const { data, content: rawBody } = (0, gray_matter_1.default)(content);
17028
- const body = stripFencedBlocks(rawBody);
17043
+ const body = stripHtmlComments(stripFencedBlocks(rawBody));
17029
17044
  const sections = splitSections(body);
17030
17045
  const parsedVerification = extractVerification(sections);
17031
17046
  // The public schema allows verification as a flat string array; read it as manual checks
@@ -17108,6 +17123,28 @@ function stripFencedBlocks(body) {
17108
17123
  }
17109
17124
  return out.join('\n');
17110
17125
  }
17126
+ /**
17127
+ * Remove HTML comments before any structural parsing. Template scaffolds ship guidance inside
17128
+ * `<!-- ... -->`, and it is invisible when the markdown renders, so a reader that treats it as
17129
+ * content is reading something the author never wrote.
17130
+ *
17131
+ * Found by running the published preflight over an untouched `pathmode-intent` scaffold: the
17132
+ * Objective section held only a comment explaining what an objective is, that comment named "a
17133
+ * role, a team, an operator, an agent", and those actor words carried it past
17134
+ * `isObjectiveSpecific`. The scaffold reported a PASSING objective nobody had written. A first-run
17135
+ * verdict that praises an empty template is worse than one that blocks, because it is confidently
17136
+ * wrong at exactly the moment a new user is deciding whether to trust the tool at all.
17137
+ *
17138
+ * Applied AFTER `stripFencedBlocks`, so a `<!--` appearing as example content inside a fence is
17139
+ * already gone and cannot open a comment that eats real prose. An unterminated comment swallows
17140
+ * the rest of the document, matching both the fence rule above and what a markdown renderer does:
17141
+ * the parser reads what the author sees.
17142
+ */
17143
+ function stripHtmlComments(body) {
17144
+ const withoutClosed = body.replace(/<!--[\s\S]*?-->/g, '');
17145
+ const dangling = withoutClosed.indexOf('<!--');
17146
+ return dangling === -1 ? withoutClosed : withoutClosed.slice(0, dangling);
17147
+ }
17111
17148
  function splitSections(body) {
17112
17149
  const sections = new Map();
17113
17150
  let current = null;
@@ -17330,6 +17367,178 @@ function hasVerificationContent(v) {
17330
17367
  }
17331
17368
 
17332
17369
 
17370
+ /***/ }),
17371
+
17372
+ /***/ 1615:
17373
+ /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
17374
+
17375
+ "use strict";
17376
+
17377
+ /**
17378
+ * The product-judgment adapter for an OpenSpec change.
17379
+ *
17380
+ * WHY THIS IS A SEPARATE LAYER. `scripts/openspec-corpus/openspec-reader.ts` is a frozen
17381
+ * instrument: the corpus measurements in docs/research/openspec-corpus-audit.md are only
17382
+ * reproducible while it keeps reading exactly what it read then (that is also why it derives the
17383
+ * title from the directory name rather than an H1, and why `openspec-import.ts` layers the nicer
17384
+ * title on top instead of editing it). Teaching that file about `intent.md` would silently
17385
+ * invalidate every recorded rate. So preference lives here, above it, and the reader stays
17386
+ * untouched.
17387
+ *
17388
+ * WHAT THIS FIXES. The `pathmode-intent` schema writes the product judgment to a change-local
17389
+ * `intent.md`, ahead of the proposal. The corpus reader has never heard of that file, so before
17390
+ * this adapter existed, a change authored faithfully through our own schema graded as
17391
+ * "nothing found" on five of six dimensions: we would have shipped a schema whose output our own
17392
+ * gate accused of having no product judgment. That is the false-accusation failure the whole
17393
+ * unconfirmed-not-absent program exists to remove, so it must not be reintroduced by two of our
17394
+ * own tools disagreeing about where judgment lives.
17395
+ *
17396
+ * THE PRECEDENCE RULES, and each one is a decision rather than a default:
17397
+ *
17398
+ * 1. A recognized change-local `intent.md` is AUTHORITATIVE for the product fields. It was
17399
+ * written to answer exactly these questions, in a format built for them.
17400
+ * 2. No `intent.md` -> fall back to `openSpecChangeToSpec()` unchanged, so a standard OpenSpec
17401
+ * repo grades exactly as it did before this file existed. Pinned by test.
17402
+ * 3. An `intent.md` that exists but cannot be read is an ERROR, never a silent fallback. Quietly
17403
+ * grading proposal.md instead would report a verdict about a document the author did not
17404
+ * write, and hide the broken file that caused it.
17405
+ * 4. Blank fields are NEVER backfilled from proposal.md. A missing objective in an intent.md is
17406
+ * an unfinished product judgment, and the gate saying so is the product working. Merging the
17407
+ * proposal's `## Why` over the gap would manufacture a pass out of a document written to
17408
+ * answer a different question.
17409
+ *
17410
+ * Read-only throughout: nothing here writes, moves, or creates a file.
17411
+ */
17412
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17413
+ return (mod && mod.__esModule) ? mod : { "default": mod };
17414
+ };
17415
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
17416
+ exports.CHANGE_LOCAL_INTENT_FILE = void 0;
17417
+ exports.isProductReadError = isProductReadError;
17418
+ exports.readOpenSpecChangeForPathmode = readOpenSpecChangeForPathmode;
17419
+ const fs_1 = __importDefault(__nccwpck_require__(9896));
17420
+ const path_1 = __importDefault(__nccwpck_require__(6928));
17421
+ const openspec_reader_1 = __nccwpck_require__(7290);
17422
+ const local_reader_1 = __nccwpck_require__(8963);
17423
+ /** The file the `pathmode-intent` schema's `product-intent` artifact generates. */
17424
+ exports.CHANGE_LOCAL_INTENT_FILE = 'intent.md';
17425
+ function isProductReadError(r) {
17426
+ return r.error !== undefined;
17427
+ }
17428
+ /**
17429
+ * Does a verification object hold anything?
17430
+ *
17431
+ * A deliberately tiny local predicate rather than `toVerificationChecks` from the intent
17432
+ * compiler: importing that pulled roughly a thousand generated lines of the compiler into the
17433
+ * published CLI bundle, and nothing here needs to normalize checks. The verification object
17434
+ * travels verbatim, so all this has to answer is "is there content".
17435
+ */
17436
+ function hasVerificationContent(v) {
17437
+ if (!v || typeof v !== 'object')
17438
+ return false;
17439
+ const src = v;
17440
+ return Boolean(src.checks?.length || src.manualChecks?.length || src.unitTests?.length || src.e2eTests?.length);
17441
+ }
17442
+ /**
17443
+ * Does this parse carry any product judgment at all?
17444
+ *
17445
+ * Title alone does not count: `parseIntentMarkdown` defaults it to 'Untitled Intent', so a file
17446
+ * of pure comments would otherwise read as "recognized" and grade as five empty gates.
17447
+ */
17448
+ function carriesJudgment(intent) {
17449
+ return Boolean((intent.objective || '').trim() ||
17450
+ (intent.outcomes || []).length ||
17451
+ (intent.constraints || []).length ||
17452
+ (intent.edgeCases || []).length ||
17453
+ hasVerificationContent(intent.verification));
17454
+ }
17455
+ /** Project a parsed intent.md onto the six-gate shape, keeping what makes it an intent.md. */
17456
+ function intentToProjection(intent) {
17457
+ const spec = {
17458
+ // The title is taken as written, with NO fallback to the change-directory name. An
17459
+ // earlier version substituted the directory name whenever it saw the 'Untitled Intent'
17460
+ // placeholder, which quietly handed a passing title gate to a scaffold nobody had filled
17461
+ // in: the directory is always named something, so the gate could never report an
17462
+ // unnamed change. `parseIntentMarkdown` already defaults a title-less file to 'Untitled
17463
+ // Intent', and the gate knows that string is a placeholder, so passing it through is
17464
+ // what makes an unfinished file read as unfinished.
17465
+ title: intent.title || '',
17466
+ objective: (intent.objective || '').trim(),
17467
+ outcomes: (intent.outcomes || []).map(o => (typeof o === 'string' ? o : String(o?.text ?? ''))).filter(Boolean),
17468
+ constraints: (intent.constraints || []).map(c => (typeof c === 'string' ? c : String(c ?? ''))).filter(Boolean),
17469
+ edgeCases: (intent.edgeCases || [])
17470
+ .map(ec => ({ scenario: String(ec?.scenario ?? '').trim(), expectedBehavior: String(ec?.expectedBehavior ?? '').trim() }))
17471
+ .filter(ec => ec.scenario || ec.expectedBehavior),
17472
+ // Verbatim: kinds are semantics, not decoration.
17473
+ verification: intent.verification ?? {},
17474
+ hasContent: true,
17475
+ };
17476
+ // Only set the key when the file actually carried one, so a spec with no Confirmations
17477
+ // section is indistinguishable from one that never had the concept.
17478
+ if (intent.confirmations?.length)
17479
+ spec.confirmations = intent.confirmations;
17480
+ return spec;
17481
+ }
17482
+ /**
17483
+ * Read a change's product judgment, preferring a change-local intent.md.
17484
+ *
17485
+ * `readFile` is injectable so tests can simulate an unreadable file (a permissions error, a
17486
+ * directory where a file belongs) without depending on the filesystem to misbehave on cue.
17487
+ */
17488
+ function readOpenSpecChangeForPathmode(ref, io = {}) {
17489
+ const exists = io.exists ?? ((p) => { try {
17490
+ return fs_1.default.existsSync(p);
17491
+ }
17492
+ catch {
17493
+ return false;
17494
+ } });
17495
+ const readFile = io.readFile ?? ((p) => fs_1.default.readFileSync(p, 'utf-8'));
17496
+ const intentPath = path_1.default.join(ref.dir, exports.CHANGE_LOCAL_INTENT_FILE);
17497
+ if (!exists(intentPath)) {
17498
+ // Rule 2: a standard OpenSpec repo is graded exactly as before.
17499
+ return { spec: (0, openspec_reader_1.openSpecChangeToSpec)(ref), source: 'openspec-artifacts', notes: [] };
17500
+ }
17501
+ let raw;
17502
+ try {
17503
+ raw = readFile(intentPath);
17504
+ }
17505
+ catch (e) {
17506
+ // Rule 3. Name the file and the reason; do not grade something else instead.
17507
+ return { error: `Found ${exports.CHANGE_LOCAL_INTENT_FILE} in this change but could not read it: ${e instanceof Error ? e.message : String(e)}`, intentPath };
17508
+ }
17509
+ let intent;
17510
+ try {
17511
+ intent = (0, local_reader_1.parseIntentMarkdown)(raw, ref.id);
17512
+ }
17513
+ catch (e) {
17514
+ // gray-matter throws on malformed frontmatter. Same rule: an explicit failure beats a
17515
+ // verdict about a different document.
17516
+ return { error: `Found ${exports.CHANGE_LOCAL_INTENT_FILE} in this change but could not parse it: ${e instanceof Error ? e.message : String(e)}`, intentPath };
17517
+ }
17518
+ if (!carriesJudgment(intent)) {
17519
+ return {
17520
+ 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.`,
17521
+ intentPath,
17522
+ };
17523
+ }
17524
+ const notes = [
17525
+ `Product judgment read from ${exports.CHANGE_LOCAL_INTENT_FILE}; proposal.md, design.md, spec deltas, and tasks.md were not merged into these fields.`,
17526
+ ];
17527
+ // Rule 4, said out loud rather than inferred from an empty verdict line.
17528
+ const blank = [
17529
+ !(intent.objective || '').trim() && 'objective',
17530
+ !(intent.outcomes || []).length && 'outcomes',
17531
+ !(intent.constraints || []).length && 'constraints',
17532
+ !(intent.edgeCases || []).length && 'edge cases',
17533
+ !hasVerificationContent(intent.verification) && 'verification',
17534
+ ].filter(Boolean);
17535
+ if (blank.length) {
17536
+ 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.`);
17537
+ }
17538
+ return { spec: intentToProjection(intent), source: 'intent.md', intentPath, notes };
17539
+ }
17540
+
17541
+
17333
17542
  /***/ }),
17334
17543
 
17335
17544
  /***/ 2804:
@@ -24428,6 +24637,14 @@ module.exports = /*#__PURE__*/JSON.parse('{"$id":"https://raw.githubusercontent.
24428
24637
  "use strict";
24429
24638
  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
24639
 
24640
+ /***/ }),
24641
+
24642
+ /***/ 8330:
24643
+ /***/ ((module) => {
24644
+
24645
+ "use strict";
24646
+ module.exports = {"rE":"2.1.3"};
24647
+
24431
24648
  /***/ })
24432
24649
 
24433
24650
  /******/ });
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@pathmode/cli",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "description": "The Intent Layer CLI \u2014 Spec-Driven Development with Pathmode",
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"