paqad-ai 1.63.2 → 1.63.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # paqad-ai
2
2
 
3
+ ## 1.63.3
4
+
5
+ ### Patch Changes
6
+
7
+ - eb1fa9a: fix(#401): spec freeze now runs the spec-quality review it always claimed to require
8
+
9
+ `paqad-ai spec freeze` told you, in both the rule pack and the workflow contract, that
10
+ freezing enforces "no critical spec-review defects". It never did: the CLI evaluated the
11
+ freeze with no review attached, so that clause was enforced nowhere. Agents closed the gap by
12
+ hand-running `compliance review`, which left a stray `.paqad/compliance/<slug>/spec-review.json`
13
+ behind that was neither part of the change's evidence bundle nor git-ignored.
14
+
15
+ Freeze now runs the review itself and blocks on a critical defect like any other blocker, so
16
+ there is no second command to run and no stray artifact. On a clean freeze the defect summary
17
+ is folded into the bundle's `specification.json`, so the review evidence travels with the spec
18
+ of record. Non-critical findings never block.
19
+
20
+ Two related fixes: `spec freeze` and `compliance review` now reject a spec file that resolves
21
+ outside the project root and record `spec_file` as a project-relative posix path, and the
22
+ managed `.paqad/.gitignore` covers `compliance/` so a compliance artifact can never surface as
23
+ an untracked, committable file.
24
+
3
25
  ## 1.63.2
4
26
 
5
27
  ### Patch Changes
package/dist/cli/index.js CHANGED
@@ -2903,9 +2903,29 @@ var init_feature_spec_schema = __esm({
2903
2903
  },
2904
2904
  frozen: {
2905
2905
  oneOf: [{ type: "null" }, { $ref: "#/$defs/frozenMetadata" }]
2906
- }
2906
+ },
2907
+ spec_review: { $ref: "#/$defs/specReviewSummary" }
2907
2908
  },
2908
2909
  $defs: {
2910
+ specReviewSummary: {
2911
+ type: "object",
2912
+ additionalProperties: false,
2913
+ required: ["reviewed_at", "defect_count", "by_severity"],
2914
+ properties: {
2915
+ reviewed_at: { type: "string", format: "date-time" },
2916
+ defect_count: { type: "integer", minimum: 0 },
2917
+ by_severity: {
2918
+ type: "object",
2919
+ additionalProperties: false,
2920
+ required: ["critical", "major", "minor"],
2921
+ properties: {
2922
+ critical: { type: "integer", minimum: 0 },
2923
+ major: { type: "integer", minimum: 0 },
2924
+ minor: { type: "integer", minimum: 0 }
2925
+ }
2926
+ }
2927
+ }
2928
+ },
2909
2929
  verificationCase: {
2910
2930
  type: "object",
2911
2931
  additionalProperties: false,
@@ -8161,6 +8181,7 @@ function defaultFeatureDevelopmentPolicy() {
8161
8181
  instructions: [
8162
8182
  "Write or refine the feature specification before implementation when the lane includes specification.",
8163
8183
  "Spec sign-off (issue #102): on graduated/full lanes the spec must carry behaviour, acceptance criteria (AC-n, given/when/then, proof_type), and human-confirmed invariants (INV-n), and must be frozen before development. Freeze requires no open questions, no critical spec-review defects, and a confirmed invariant set. A mid-build goal change or a work-vs-spec contradiction surfaces via the Decision Pause Contract (spec.change / spec.contradiction) and is never resolved silently.",
8184
+ SPEC_REVIEW_ENFORCEMENT_INSTRUCTION,
8164
8185
  "Freeze the spec before writing code on graduated/full lanes: run `npx paqad-ai spec freeze <spec-file> --signed-off-by <name> --confirm-invariants` and resolve every printed blocker (missing ACs/invariants, open questions) before development. It writes the frozen spec into the active feature bundle (`specification.json`) that development builds against and the spec-change guard checks for drift."
8165
8186
  ],
8166
8187
  required_inputs: ["approved spec boundary"],
@@ -8543,6 +8564,7 @@ stages:
8543
8564
  instructions:
8544
8565
  - Write or refine the feature specification before implementation when the lane includes specification.
8545
8566
  - "Spec sign-off (issue #102): on graduated/full lanes the spec must carry behaviour, acceptance criteria (AC-n, given/when/then, proof_type), and human-confirmed invariants (INV-n), and must be frozen before development. Freeze requires no open questions, no critical spec-review defects, and a confirmed invariant set. A mid-build goal change or a work-vs-spec contradiction surfaces via the Decision Pause Contract (spec.change / spec.contradiction) and is never resolved silently."
8567
+ - "${SPEC_REVIEW_ENFORCEMENT_INSTRUCTION}"
8546
8568
  - "Freeze the spec before writing code on graduated/full lanes: run \`npx paqad-ai spec freeze <spec-file> --signed-off-by <name> --confirm-invariants\` and resolve every printed blocker (missing ACs/invariants, open questions) before development. It writes the frozen spec into the active feature bundle (\`specification.json\`) that development builds against and the spec-change guard checks for drift."
8547
8569
  required_inputs:
8548
8570
  - approved spec boundary
@@ -8649,7 +8671,7 @@ stages:
8649
8671
  - CI gate result
8650
8672
  `;
8651
8673
  }
8652
- var STAGE_ORDER, REQUIRED_TRUE_STRICTNESS;
8674
+ var STAGE_ORDER, REQUIRED_TRUE_STRICTNESS, SPEC_REVIEW_ENFORCEMENT_INSTRUCTION;
8653
8675
  var init_feature_development_policy = __esm({
8654
8676
  "src/pipeline/feature-development-policy.ts"() {
8655
8677
  "use strict";
@@ -8674,6 +8696,7 @@ var init_feature_development_policy = __esm({
8674
8696
  checks: { block_on_failure: true },
8675
8697
  documentation_sync: { require_canonical_sync: true }
8676
8698
  };
8699
+ SPEC_REVIEW_ENFORCEMENT_INSTRUCTION = "Spec-review enforcement (issue #401): SCRIPT-ENFORCED. `paqad-ai spec freeze` runs the spec-quality review itself and refuses to freeze while a critical defect is open, so never run `compliance review` by hand to satisfy the clause above. Major and minor findings never block. The defect summary is folded into the bundle's specification.json and no .paqad/compliance/<slug>/spec-review.json is written by this workflow. Author the spec markdown inside the project: a path resolving outside the project root is rejected.";
8677
8700
  }
8678
8701
  });
8679
8702
 
@@ -25660,7 +25683,13 @@ var MANAGED_GITIGNORE_ENTRIES = [
25660
25683
  "delivery-detection.json",
25661
25684
  // regenerated from git history per machine
25662
25685
  "# compliance ledger (share via dashboard/SIEM, not git)",
25663
- "ledger/"
25686
+ "ledger/",
25687
+ // Issue #401 — everything under `compliance/` (the obligation index, the compliance
25688
+ // report, a spec-review report) is derived from the spec plus the test tree and is
25689
+ // regenerable, exactly the `ledger/` rationale. It was NOT ignored, so a report written
25690
+ // there surfaced as an untracked, committable artifact in a consumer project and fed the
25691
+ // "untracked .paqad/ artifact leaks into the changed-file set" class tracked by #205.
25692
+ "compliance/"
25664
25693
  ];
25665
25694
  var MANAGED_GITATTRIBUTES_ENTRIES = ["decisions/index.json merge=union"];
25666
25695
  var DEPRECATED_ARTIFACTS = [
@@ -30883,7 +30912,7 @@ init_cancelled_error();
30883
30912
  init_events();
30884
30913
 
30885
30914
  // src/index.ts
30886
- var VERSION = "1.63.2";
30915
+ var VERSION = "1.63.3";
30887
30916
 
30888
30917
  // src/cli/commands/audit.ts
30889
30918
  init_esm_shims();
@@ -32018,6 +32047,42 @@ init_esm_shims();
32018
32047
  import { readFile as readFile68, stat as stat7 } from "fs/promises";
32019
32048
  import path10 from "path";
32020
32049
  import { Command as Command4 } from "commander";
32050
+
32051
+ // src/stage-evidence/artifact-path.ts
32052
+ init_esm_shims();
32053
+ import { realpathSync } from "fs";
32054
+ import { isAbsolute as isAbsolute6, relative as relative24, resolve as resolve7 } from "pathe";
32055
+ var ArtifactOutOfTreeError = class extends Error {
32056
+ constructor(input3) {
32057
+ super(`artifact must be a path inside the project; got ${input3}`);
32058
+ this.input = input3;
32059
+ this.name = "ArtifactOutOfTreeError";
32060
+ }
32061
+ };
32062
+ function realOrLexical(p) {
32063
+ try {
32064
+ return realpathSync.native(p);
32065
+ } catch {
32066
+ return p;
32067
+ }
32068
+ }
32069
+ function isEscape(rel) {
32070
+ return rel === "" || rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute6(rel);
32071
+ }
32072
+ function normalizeArtifactPath(projectRoot, input3) {
32073
+ const rootResolved = resolve7(projectRoot);
32074
+ const rootReal = realOrLexical(rootResolved);
32075
+ const absInput = isAbsolute6(input3) ? realOrLexical(input3) : resolve7(rootReal, input3);
32076
+ for (const root of /* @__PURE__ */ new Set([rootReal, rootResolved])) {
32077
+ const rel = relative24(root, absInput);
32078
+ if (!isEscape(rel)) {
32079
+ return rel.replace(/\\/g, "/");
32080
+ }
32081
+ }
32082
+ throw new ArtifactOutOfTreeError(input3);
32083
+ }
32084
+
32085
+ // src/cli/commands/compliance.ts
32021
32086
  function resolveIndexPath2(explicitPath, spec) {
32022
32087
  if (explicitPath) return explicitPath;
32023
32088
  if (spec) return specIndexPath(spec);
@@ -32076,8 +32141,17 @@ function createComplianceCommand() {
32076
32141
  process.exitCode = 0;
32077
32142
  });
32078
32143
  command.command("review <specFile>").description("Run deterministic spec-quality review and persist the review report").option("--project-root <path>", "Project root", process.cwd()).option("--json", "Print JSON instead of a human-readable summary").action(async (specFile, options) => {
32079
- const specPath = path10.resolve(options.projectRoot, specFile);
32080
- const relativeSpec = path10.relative(options.projectRoot, specPath);
32144
+ let relativeSpec;
32145
+ try {
32146
+ relativeSpec = normalizeArtifactPath(options.projectRoot, specFile);
32147
+ } catch {
32148
+ console.error(
32149
+ `**\u25B8 paqad** \xB7 a spec has to live inside the project for its review to be portable, and ${specFile} resolves outside it.`
32150
+ );
32151
+ process.exitCode = 1;
32152
+ return;
32153
+ }
32154
+ const specPath = path10.resolve(options.projectRoot, relativeSpec);
32081
32155
  const specMarkdown = await readFile68(specPath, "utf8");
32082
32156
  const previousReport = await loadSpecReviewReport({
32083
32157
  project_root: options.projectRoot,
@@ -32407,7 +32481,7 @@ function resolveRelativeSpec(projectRoot, spec) {
32407
32481
  init_esm_shims();
32408
32482
  init_paths();
32409
32483
  import { existsSync as existsSync105 } from "fs";
32410
- import { dirname as dirname79, isAbsolute as isAbsolute6, join as join194, resolve as resolve10 } from "path";
32484
+ import { dirname as dirname79, isAbsolute as isAbsolute7, join as join194, resolve as resolve11 } from "path";
32411
32485
  import { fileURLToPath as fileURLToPath3 } from "url";
32412
32486
  import { Command as Command5 } from "commander";
32413
32487
 
@@ -32415,7 +32489,7 @@ import { Command as Command5 } from "commander";
32415
32489
  init_esm_shims();
32416
32490
  import { createReadStream, existsSync as existsSync104, statSync as statSync13 } from "fs";
32417
32491
  import { createServer } from "http";
32418
- import { extname as extname9, join as join193, normalize as normalize2, resolve as resolve9 } from "path";
32492
+ import { extname as extname9, join as join193, normalize as normalize2, resolve as resolve10 } from "path";
32419
32493
  import { createGzip, gzipSync as gzipSync2 } from "zlib";
32420
32494
  init_paths();
32421
32495
  init_logger_registry();
@@ -32505,7 +32579,7 @@ function buildNodeDetail(graph, nodeId, options) {
32505
32579
  init_esm_shims();
32506
32580
  import { existsSync as existsSync75 } from "fs";
32507
32581
  import { readFile as readFile69, readdir as readdir15, stat as stat8 } from "fs/promises";
32508
- import { extname as extname8, join as join161, relative as relative24, sep as sep6 } from "path";
32582
+ import { extname as extname8, join as join161, relative as relative25, sep as sep6 } from "path";
32509
32583
  init_paths();
32510
32584
  var KNOWN_TIERS = /* @__PURE__ */ new Set(["green", "amber", "red", "unknown"]);
32511
32585
  function normaliseTier(value) {
@@ -32543,7 +32617,7 @@ function languageFromExtension(filePath) {
32543
32617
  return map[ext] ?? null;
32544
32618
  }
32545
32619
  function toProjectRelative2(projectRoot, absolutePath) {
32546
- const rel = relative24(projectRoot, absolutePath);
32620
+ const rel = relative25(projectRoot, absolutePath);
32547
32621
  return rel === "" ? "." : rel.split(sep6).join("/");
32548
32622
  }
32549
32623
  function moduleNameForFile(relativePath, knownModules) {
@@ -33437,11 +33511,11 @@ import {
33437
33511
  lstatSync as lstatSync2,
33438
33512
  mkdirSync as mkdirSync47,
33439
33513
  readFileSync as readFileSync76,
33440
- realpathSync,
33514
+ realpathSync as realpathSync2,
33441
33515
  renameSync as renameSync13,
33442
33516
  writeFileSync as writeFileSync39
33443
33517
  } from "fs";
33444
- import { dirname as dirname77, join as join166, resolve as resolve7, sep as sep7 } from "path";
33518
+ import { dirname as dirname77, join as join166, resolve as resolve8, sep as sep7 } from "path";
33445
33519
  var WriteConflictError = class extends Error {
33446
33520
  currentContent;
33447
33521
  currentHash;
@@ -33490,8 +33564,8 @@ function resolveManagedPath(projectRoot, relativePath) {
33490
33564
  if (!ALLOWED_EXTENSIONS.some((extension) => posix4.endsWith(extension))) {
33491
33565
  throw new PathNotAllowedError(relativePath, "extension is not editable");
33492
33566
  }
33493
- const realRoot = existsSync79(projectRoot) ? realpathSync(resolve7(projectRoot)) : resolve7(projectRoot);
33494
- const absolute = resolve7(realRoot, posix4);
33567
+ const realRoot = existsSync79(projectRoot) ? realpathSync2(resolve8(projectRoot)) : resolve8(projectRoot);
33568
+ const absolute = resolve8(realRoot, posix4);
33495
33569
  const rootPrefix = `${realRoot}${sep7}`;
33496
33570
  if (!absolute.startsWith(rootPrefix)) {
33497
33571
  throw new PathNotAllowedError(relativePath, "outside the project root");
@@ -33502,7 +33576,7 @@ function resolveManagedPath(projectRoot, relativePath) {
33502
33576
  if (lstatSync2(probe).isSymbolicLink()) {
33503
33577
  throw new PathNotAllowedError(relativePath, "symlinks are not editable");
33504
33578
  }
33505
- const real = realpathSync(probe);
33579
+ const real = realpathSync2(probe);
33506
33580
  if (real !== probe && !real.startsWith(rootPrefix)) {
33507
33581
  throw new PathNotAllowedError(relativePath, "resolves outside the project root");
33508
33582
  }
@@ -35413,7 +35487,7 @@ function removePack2(projectRoot, input3) {
35413
35487
  init_esm_shims();
35414
35488
  init_paths();
35415
35489
  import { existsSync as existsSync101 } from "fs";
35416
- import { join as join190, resolve as resolve8 } from "path";
35490
+ import { join as join190, resolve as resolve9 } from "path";
35417
35491
 
35418
35492
  // src/dashboard/collectors/architecture.ts
35419
35493
  init_esm_shims();
@@ -37188,7 +37262,7 @@ function computeOverall(sections) {
37188
37262
  }
37189
37263
  function buildReport(projectRoot, options = {}) {
37190
37264
  const now = options.now ?? Date.now();
37191
- const root = resolve8(projectRoot);
37265
+ const root = resolve9(projectRoot);
37192
37266
  const generatedAt = new Date(now).toISOString();
37193
37267
  const paqadDir = join190(root, PATHS.AGENCY_DIR);
37194
37268
  const manifestPath = join190(root, PATHS.ONBOARDING_MANIFEST);
@@ -37491,8 +37565,8 @@ function writeText(res, body, status = 200, contentType = "text/plain; charset=u
37491
37565
  function serveStatic(req, res, staticDir, urlPath) {
37492
37566
  const safePath = normalize2(urlPath).replace(/^[/\\]+/, "");
37493
37567
  let filePath = join193(staticDir, safePath);
37494
- const resolved = resolve9(filePath);
37495
- if (!resolved.startsWith(resolve9(staticDir))) {
37568
+ const resolved = resolve10(filePath);
37569
+ if (!resolved.startsWith(resolve10(staticDir))) {
37496
37570
  writeText(res, "Forbidden", 403);
37497
37571
  return;
37498
37572
  }
@@ -38226,12 +38300,12 @@ function openBrowser(options) {
38226
38300
  var DEFAULT_PORT = 5372;
38227
38301
  function resolveStaticDir(override) {
38228
38302
  if (override) {
38229
- return isAbsolute6(override) ? override : resolve10(process.cwd(), override);
38303
+ return isAbsolute7(override) ? override : resolve11(process.cwd(), override);
38230
38304
  }
38231
38305
  const here = dirname79(fileURLToPath3(import.meta.url));
38232
38306
  const candidates = [
38233
- resolve10(here, "../../runtime/graph-ui"),
38234
- resolve10(here, "../../../runtime/graph-ui")
38307
+ resolve11(here, "../../runtime/graph-ui"),
38308
+ resolve11(here, "../../../runtime/graph-ui")
38235
38309
  ];
38236
38310
  for (const c of candidates) {
38237
38311
  if (existsSync105(join194(c, "index.html"))) return c;
@@ -38240,7 +38314,7 @@ function resolveStaticDir(override) {
38240
38314
  }
38241
38315
  function createDashboardCommand() {
38242
38316
  return new Command5("dashboard").description("Open the paqad-ai project dashboard in a local web view").option("--port <n>", "Server port (auto-increments if occupied)", String(DEFAULT_PORT)).option("--host <host>", "Bind address", "127.0.0.1").option("--no-open", "Do not open the browser automatically").option("--no-watch", "Disable live reload on .paqad/ changes").option("--quiet", "Suppress non-essential stdout", false).option("--read-only", "Disable every mutation endpoint (for shared or CI usage)", false).option("--project-root <path>", "Project root", process.cwd()).option("--static-dir <path>", "Override the bundled frontend directory").action(async (options) => {
38243
- const projectRoot = resolve10(options.projectRoot);
38317
+ const projectRoot = resolve11(options.projectRoot);
38244
38318
  const paqadDir = join194(projectRoot, PATHS.AGENCY_DIR);
38245
38319
  if (!existsSync105(paqadDir)) {
38246
38320
  process.stderr.write(
@@ -38320,7 +38394,7 @@ Received ${signal}, shutting down\u2026
38320
38394
  init_esm_shims();
38321
38395
  init_paths();
38322
38396
  import { existsSync as existsSync106, readFileSync as readFileSync92, readdirSync as readdirSync29 } from "fs";
38323
- import { join as join195, resolve as resolve11 } from "path";
38397
+ import { join as join195, resolve as resolve12 } from "path";
38324
38398
  import { Command as Command6 } from "commander";
38325
38399
 
38326
38400
  // src/delivery/ci-gate.ts
@@ -38770,7 +38844,7 @@ function loadEvidenceBody(projectRoot) {
38770
38844
  }
38771
38845
  function createDeliveryCommand() {
38772
38846
  return new Command6("deliver").description("Run the delivery chain: PR (after asking) \u2192 wait for CI \u2192 evidence on green").option("--project-root <path>", "Project root", process.cwd()).option("--dry-run", "Render branch/commit/PR text without pushing", false).option("--base <branch>", "Base branch for the PR", "main").option("--draft", "Open the PR as a draft", false).option("--title <title>", "Change title (branch slug + PR title)", "").option("--summary <summary>", "One-line summary for the commit/PR body", "").option("--ticket <ref>", "Linked ticket ref (e.g. #45 or PQD-123)").option("--ticket-type <type>", "Ticket type (Story/Bug/Task) \u2192 conventional commit type").option("--scope <scope>", "Conventional-commit scope (usually the module)").option("--open-pr <mode>", "Recommended open_pr choice for the pause (yes|draft|no)", "yes").option("--reviewer <handle>", "PR reviewer (repeatable)", collect, []).option("--label <label>", "PR label (repeatable)", collect, []).action(async (options) => {
38773
- const projectRoot = resolve11(options.projectRoot);
38847
+ const projectRoot = resolve12(options.projectRoot);
38774
38848
  const shell = createDeliveryShell(projectRoot);
38775
38849
  const host = new GithubHostProvider(shell);
38776
38850
  const result = await runDeliver({
@@ -38967,11 +39041,11 @@ function createEnableCommand() {
38967
39041
  // src/cli/commands/evidence.ts
38968
39042
  init_esm_shims();
38969
39043
  import { mkdirSync as mkdirSync49, writeFileSync as writeFileSync41 } from "fs";
38970
- import { dirname as dirname80, resolve as resolve12 } from "path";
39044
+ import { dirname as dirname80, resolve as resolve13 } from "path";
38971
39045
  import { Command as Command11 } from "commander";
38972
39046
  function createEvidenceCommand() {
38973
39047
  return new Command11("evidence").description("Render verification evidence as a scannable PR comment (Markdown or JSON)").argument("[sha]", "Commit SHA or label shown in the headline").option("--format <fmt>", "Output format: markdown | json", "markdown").option("--project-root <path>", "Project root", process.cwd()).option("--output <path>", "Write to a file instead of stdout (for gh pr comment --body-file)").option("--fail-on-red", "Exit non-zero when the overall status is fail", false).action((sha, options) => {
38974
- const projectRoot = resolve12(options.projectRoot);
39048
+ const projectRoot = resolve13(options.projectRoot);
38975
39049
  const fmt = options.format.toLowerCase();
38976
39050
  if (fmt !== "markdown" && fmt !== "json") {
38977
39051
  process.stderr.write(
@@ -38992,7 +39066,7 @@ function createEvidenceCommand() {
38992
39066
  }
38993
39067
  const output = fmt === "json" ? JSON.stringify(evidence, null, 2) : renderEvidenceMarkdown(evidence, sha ? { sha } : {});
38994
39068
  if (options.output) {
38995
- const target = resolve12(options.output);
39069
+ const target = resolve13(options.output);
38996
39070
  mkdirSync49(dirname80(target), { recursive: true });
38997
39071
  writeFileSync41(target, `${output}
38998
39072
  `, "utf8");
@@ -39104,19 +39178,19 @@ function createFeatureCommand() {
39104
39178
  init_esm_shims();
39105
39179
  init_paths();
39106
39180
  import { existsSync as existsSync107 } from "fs";
39107
- import { dirname as dirname81, isAbsolute as isAbsolute7, join as join196, resolve as resolve13 } from "path";
39181
+ import { dirname as dirname81, isAbsolute as isAbsolute8, join as join196, resolve as resolve14 } from "path";
39108
39182
  import { fileURLToPath as fileURLToPath4 } from "url";
39109
39183
  import { Command as Command13 } from "commander";
39110
39184
  var DEFAULT_PORT2 = 5372;
39111
39185
  var DEPRECATION_LINE = "paqad-ai graph has moved into the dashboard. Opening the dashboard on the Graph view. Run paqad-ai dashboard next time.";
39112
39186
  function resolveStaticDir2(override) {
39113
39187
  if (override) {
39114
- return isAbsolute7(override) ? override : resolve13(process.cwd(), override);
39188
+ return isAbsolute8(override) ? override : resolve14(process.cwd(), override);
39115
39189
  }
39116
39190
  const here = dirname81(fileURLToPath4(import.meta.url));
39117
39191
  const candidates = [
39118
- resolve13(here, "../../runtime/graph-ui"),
39119
- resolve13(here, "../../../runtime/graph-ui")
39192
+ resolve14(here, "../../runtime/graph-ui"),
39193
+ resolve14(here, "../../../runtime/graph-ui")
39120
39194
  ];
39121
39195
  for (const c of candidates) {
39122
39196
  if (existsSync107(join196(c, "index.html"))) return c;
@@ -39125,7 +39199,7 @@ function resolveStaticDir2(override) {
39125
39199
  }
39126
39200
  function createGraphCommand() {
39127
39201
  return new Command13("graph").description("Deprecated: opens the dashboard on the Graph view").option("--port <n>", "Server port (auto-increments if occupied)", String(DEFAULT_PORT2)).option("--host <host>", "Bind address", "127.0.0.1").option("--no-open", "Do not open the browser automatically").option("--no-watch", "Disable live reload on .paqad/ changes").option("--quiet", "Suppress non-essential stdout", false).option("--project-root <path>", "Project root", process.cwd()).option("--static-dir <path>", "Override the bundled frontend directory").action(async (options) => {
39128
- const projectRoot = resolve13(options.projectRoot);
39202
+ const projectRoot = resolve14(options.projectRoot);
39129
39203
  const paqadDir = join196(projectRoot, PATHS.AGENCY_DIR);
39130
39204
  if (!existsSync107(paqadDir)) {
39131
39205
  process.stderr.write(
@@ -41338,7 +41412,7 @@ init_session();
41338
41412
  init_esm_shims();
41339
41413
  init_stage_ledger();
41340
41414
  init_session();
41341
- import { isAbsolute as isAbsolute8, relative as relative25 } from "pathe";
41415
+ import { isAbsolute as isAbsolute9, relative as relative26 } from "pathe";
41342
41416
  init_stages();
41343
41417
  function stagesWithKind(rows, kind) {
41344
41418
  const set = /* @__PURE__ */ new Set();
@@ -41495,7 +41569,7 @@ init_service();
41495
41569
  init_esm_shims();
41496
41570
  init_project_intelligence();
41497
41571
  import { readFileSync as readFileSync102 } from "fs";
41498
- import { isAbsolute as isAbsolute9, join as join213 } from "path";
41572
+ import { isAbsolute as isAbsolute10, join as join213 } from "path";
41499
41573
  import { confirm, input as input2, select as select3 } from "@inquirer/prompts";
41500
41574
  import { Command as Command18 } from "commander";
41501
41575
 
@@ -42975,7 +43049,7 @@ function createRagCommand() {
42975
43049
  readFile: (path11) => {
42976
43050
  try {
42977
43051
  return readFileSync102(
42978
- isAbsolute9(path11) ? path11 : join213(options.projectRoot, path11),
43052
+ isAbsolute10(path11) ? path11 : join213(options.projectRoot, path11),
42979
43053
  "utf8"
42980
43054
  );
42981
43055
  } catch {
@@ -44145,7 +44219,7 @@ init_framework_config();
44145
44219
  init_esm_shims();
44146
44220
  init_path_utils();
44147
44221
  init_reconciler();
44148
- import { isAbsolute as isAbsolute10, relative as relative26 } from "path";
44222
+ import { isAbsolute as isAbsolute11, relative as relative27 } from "path";
44149
44223
  function normalise(path11) {
44150
44224
  return toPosixPath(path11).replace(/^\.\//, "").replace(/\/+$/, "");
44151
44225
  }
@@ -44160,7 +44234,7 @@ function resolvePathSensitivity(projectRoot, relPath) {
44160
44234
  if (!map) {
44161
44235
  return "normal";
44162
44236
  }
44163
- const rel = isAbsolute10(relPath) ? relative26(projectRoot, relPath) : relPath;
44237
+ const rel = isAbsolute11(relPath) ? relative27(projectRoot, relPath) : relPath;
44164
44238
  const target = normalise(rel);
44165
44239
  if (target.length === 0) {
44166
44240
  return "normal";
@@ -44564,7 +44638,20 @@ function freezeSpec(spec, input3) {
44564
44638
  spec_hash: spec.spec_hash,
44565
44639
  signed_off_by: input3.signed_off_by
44566
44640
  };
44567
- return { ...spec, frozen };
44641
+ const specReview = input3.spec_review ? summarizeSpecReview2(input3.spec_review) : void 0;
44642
+ return { ...spec, frozen, ...specReview === void 0 ? {} : { spec_review: specReview } };
44643
+ }
44644
+ function summarizeSpecReview2(report) {
44645
+ const open = report.defects.filter((defect) => defect.status !== "resolved");
44646
+ return {
44647
+ reviewed_at: report.metadata.reviewed_at,
44648
+ defect_count: open.length,
44649
+ by_severity: {
44650
+ critical: open.filter((defect) => defect.severity === "critical").length,
44651
+ major: open.filter((defect) => defect.severity === "major").length,
44652
+ minor: open.filter((defect) => defect.severity === "minor").length
44653
+ }
44654
+ };
44568
44655
  }
44569
44656
  function isFrozenSpecStale(spec, currentSpecHash) {
44570
44657
  return spec.frozen !== null && spec.frozen.spec_hash !== currentSpecHash;
@@ -44675,34 +44762,6 @@ init_esm_shims();
44675
44762
  init_stage_ledger();
44676
44763
  init_session();
44677
44764
 
44678
- // src/stage-evidence/artifact-path.ts
44679
- init_esm_shims();
44680
- import { realpathSync as realpathSync2 } from "fs";
44681
- import { isAbsolute as isAbsolute11, relative as relative27, resolve as resolve14 } from "pathe";
44682
- var ArtifactOutOfTreeError = class extends Error {
44683
- constructor(input3) {
44684
- super(`artifact must be a path inside the project; got ${input3}`);
44685
- this.input = input3;
44686
- this.name = "ArtifactOutOfTreeError";
44687
- }
44688
- };
44689
- function realOrLexical(p) {
44690
- try {
44691
- return realpathSync2.native(p);
44692
- } catch {
44693
- return p;
44694
- }
44695
- }
44696
- function normalizeArtifactPath(projectRoot, input3) {
44697
- const rootAbs = realOrLexical(resolve14(projectRoot));
44698
- const absInput = isAbsolute11(input3) ? realOrLexical(input3) : resolve14(rootAbs, input3);
44699
- const rel = relative27(rootAbs, absInput);
44700
- if (rel === "" || rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute11(rel)) {
44701
- throw new ArtifactOutOfTreeError(input3);
44702
- }
44703
- return rel.replace(/\\/g, "/");
44704
- }
44705
-
44706
44765
  // src/stage-evidence/bundle-artifact.ts
44707
44766
  init_esm_shims();
44708
44767
  init_paths2();
@@ -45827,9 +45886,13 @@ function createSpecCommand() {
45827
45886
  try {
45828
45887
  relSpec = normalizeArtifactPath(options.projectRoot, specFile);
45829
45888
  } catch {
45830
- relSpec = null;
45889
+ console.error(
45890
+ `**\u25B8 paqad** \xB7 a spec has to live inside the project so its record stays portable, and ${specFile} resolves outside it. Author it in the repo and freeze it from there.`
45891
+ );
45892
+ process.exitCode = 1;
45893
+ return;
45831
45894
  }
45832
- if (relSpec !== null && classifyBundlePath(relSpec) !== null) {
45895
+ if (classifyBundlePath(relSpec) !== null) {
45833
45896
  console.error(
45834
45897
  `**\u25B8 paqad** \xB7 a feature bundle holds only its rigid artifacts, so a spec can't live at ${relSpec}. Author it outside \`.paqad/ledger/feature-evidence/\` \u2014 the freeze writes specification.json into the bundle for you.`
45835
45898
  );
@@ -45847,11 +45910,15 @@ function createSpecCommand() {
45847
45910
  const specId = options.specId ?? basename16(specFile).replace(/\.[^.]+$/, "");
45848
45911
  const built = buildFeatureSpec({
45849
45912
  spec_id: specId,
45850
- spec_file: specFile,
45913
+ spec_file: relSpec,
45851
45914
  spec_markdown: markdown
45852
45915
  });
45853
45916
  const spec = options.confirmInvariants ? { ...built, invariants: built.invariants.map((inv) => ({ ...inv, confirmed: true })) } : built;
45854
- const evaluation = evaluateSpecFreeze(spec);
45917
+ const specReview = reviewSpecification({
45918
+ spec_file: relSpec,
45919
+ spec_markdown: markdown
45920
+ });
45921
+ const evaluation = evaluateSpecFreeze(spec, specReview);
45855
45922
  if (!evaluation.can_freeze) {
45856
45923
  console.error(
45857
45924
  `**\u25B8 paqad** \xB7 can't freeze this spec yet \u2014 ${evaluation.blockers.length} blocker${evaluation.blockers.length === 1 ? "" : "s"} to resolve:`
@@ -45864,7 +45931,8 @@ function createSpecCommand() {
45864
45931
  }
45865
45932
  const frozen = freezeSpec(spec, {
45866
45933
  signed_off_by: options.signedOffBy,
45867
- frozen_at: (/* @__PURE__ */ new Date()).toISOString()
45934
+ frozen_at: (/* @__PURE__ */ new Date()).toISOString(),
45935
+ spec_review: specReview
45868
45936
  });
45869
45937
  let bundlePath = null;
45870
45938
  try {