@pathmode/cli 2.1.0 → 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.
Files changed (2) hide show
  1. package/dist/index.js +221 -9
  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.0');
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.
@@ -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:
@@ -17481,20 +17667,38 @@ exports.READINESS_BLOCKER_DESCRIPTIONS = {
17481
17667
  };
17482
17668
  /** Display order for the gate strip (title first, matching the Preflight page). */
17483
17669
  // ── Confirmation anchoring ──────────────────────────────────────────────────
17670
+ /**
17671
+ * Remove PAIRED markdown emphasis markers and nothing else.
17672
+ *
17673
+ * The earlier version replaced every asterisk in the text with nothing. That
17674
+ * was wrong in both directions at once: dropping a literal wildcard (`search * patterns` ->
17675
+ * `search patterns`) is a substantive edit that kept its confirmation live, while `_italic_` was
17676
+ * left alone so a formatting-only edit invalidated one. Emphasis needs an opening marker followed
17677
+ * by non-space and a matching closer, so a lone `*` or `_` survives. Looped because markers nest.
17678
+ */
17679
+ function stripEmphasis(text) {
17680
+ let prev;
17681
+ let out = text;
17682
+ do {
17683
+ prev = out;
17684
+ out = out.replace(/(\*\*|__|\*|_)(?=\S)([\s\S]*?\S)\1/g, '$2');
17685
+ } while (out !== prev);
17686
+ return out;
17687
+ }
17484
17688
  /**
17485
17689
  * Fold away everything that is a rewrite of the same claim, and nothing that is a change to it.
17486
17690
  * NFC because one glyph has two encodings; soft hyphens because editors inject them invisibly;
17487
- * quote/dash folding and markdown emphasis because adding `**` around a word is formatting, not
17488
- * a reword. Lowercased and whitespace-collapsed last.
17691
+ * quote/dash folding and PAIRED markdown emphasis because adding `**` around a word is
17692
+ * formatting, not a reword. An unpaired `*` or `_` is left alone: it is content (a wildcard),
17693
+ * and deleting it silently kept confirmations alive across a real change to the claim. Lowercased and whitespace-collapsed last.
17489
17694
  */
17490
17695
  function normalizeAnchor(value) {
17491
- return coerceText(value)
17696
+ return stripEmphasis(coerceText(value))
17492
17697
  .normalize('NFC')
17493
17698
  .replace(/­/g, '')
17494
17699
  .replace(/[‘’ʼ]/g, "'")
17495
17700
  .replace(/[“”]/g, '"')
17496
17701
  .replace(/[–—]/g, '-')
17497
- .replace(/\*/g, '')
17498
17702
  .replace(/\s+/g, ' ')
17499
17703
  .trim()
17500
17704
  .toLowerCase();
@@ -24410,6 +24614,14 @@ module.exports = /*#__PURE__*/JSON.parse('{"$id":"https://raw.githubusercontent.
24410
24614
  "use strict";
24411
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}');
24412
24616
 
24617
+ /***/ }),
24618
+
24619
+ /***/ 8330:
24620
+ /***/ ((module) => {
24621
+
24622
+ "use strict";
24623
+ module.exports = {"rE":"2.1.2"};
24624
+
24413
24625
  /***/ })
24414
24626
 
24415
24627
  /******/ });
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@pathmode/cli",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
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"