@webpieces/rules-config 0.4.705 → 0.4.707

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/rules-config",
3
- "version": "0.4.705",
3
+ "version": "0.4.707",
4
4
  "description": "Shared webpieces.config.json loader. Single source of truth for validation rule configuration consumed by @webpieces/ai-hook-rules, @webpieces/code-rules, and @webpieces/nx-webpieces-rules.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -0,0 +1,49 @@
1
+ import { ChecklistVerdict, RequiredChecklist } from './review-json-data';
2
+ /**
3
+ * THE renderer for "this reviewer said no" — one wording for every refusal, wherever it surfaces.
4
+ *
5
+ * It is its own class for the reason {@link ReviewerVerdictGate} is: this is the text an AI ACTS ON, so it
6
+ * is the piece most worth asserting on directly, and `review-json.ts` was already at its file-size limit.
7
+ * The behaviour is unchanged by the move — `ReviewJsonService.refusalError` delegates here, so there is
8
+ * still exactly ONE implementation and no call site has two spellings to choose between.
9
+ *
10
+ * The text was previously inlined inside review.json validation, reachable only through that path, while
11
+ * the command layer refused earlier with its own generic message. Two messages for one event is how the
12
+ * useful one became unreachable.
13
+ *
14
+ * It always quotes the reviewer's own words verbatim: the finding is the whole point, and an error that
15
+ * names a checklist without saying what it objected to gives the reader nothing to act on.
16
+ *
17
+ * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.
18
+ */
19
+ export declare class ChecklistRefusalRenderer {
20
+ /**
21
+ * One refusal, in the terms its own verdict demands.
22
+ *
23
+ * `archivedPath` non-empty ⇒ the verdict has just been RETIRED (moved) there, which changes the message
24
+ * in two ways: it says where the record went, so the move does not read as data loss, and it asks for a
25
+ * FRESH verdict file rather than pointing at a path that no longer exists.
26
+ */
27
+ render(req: RequiredChecklist, verdict: ChecklistVerdict, archivedPath?: string): string;
28
+ /**
29
+ * The refusal for an override NOBODY AUTHORIZED — a reviewer went red and then wrote its own
30
+ * ship-anyway justification, with no signed human approval on the branch behind it.
31
+ *
32
+ * Worded as a completely different event from a plain refusal, because it asks for a different action.
33
+ * A refusal says FIX THE FINDING. This says: the decision to accept the finding is not yours to make, so
34
+ * STOP and ask a person — and it names the command that person runs. Telling this reader to fix the code
35
+ * would send them to re-do work somebody may already have decided to accept, and telling them to "get an
36
+ * override" is what they already, wrongly, did.
37
+ */
38
+ private unauthorizedOverride;
39
+ /**
40
+ * The ONE sentence that names the human-authorization channel, so every refusal says it identically.
41
+ *
42
+ * Deliberately addressed to the AGENT about what to ASK FOR, not about what to run: `wp-authorize` reads
43
+ * from `/dev/tty` and an agent has none, so an agent that tries it hangs or is denied. `wp-check-auth` is
44
+ * the half an agent runs, and it is named second so there is something to do once the human is done.
45
+ */
46
+ private authorizeHint;
47
+ /** The verdict file a checklist id maps to. Duplicated nowhere — ReviewJsonService asks this class. */
48
+ checklistFileName(checklistId: string): string;
49
+ }
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChecklistRefusalRenderer = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const inversify_1 = require("inversify");
6
+ const review_json_data_1 = require("./review-json-data");
7
+ /**
8
+ * THE renderer for "this reviewer said no" — one wording for every refusal, wherever it surfaces.
9
+ *
10
+ * It is its own class for the reason {@link ReviewerVerdictGate} is: this is the text an AI ACTS ON, so it
11
+ * is the piece most worth asserting on directly, and `review-json.ts` was already at its file-size limit.
12
+ * The behaviour is unchanged by the move — `ReviewJsonService.refusalError` delegates here, so there is
13
+ * still exactly ONE implementation and no call site has two spellings to choose between.
14
+ *
15
+ * The text was previously inlined inside review.json validation, reachable only through that path, while
16
+ * the command layer refused earlier with its own generic message. Two messages for one event is how the
17
+ * useful one became unreachable.
18
+ *
19
+ * It always quotes the reviewer's own words verbatim: the finding is the whole point, and an error that
20
+ * names a checklist without saying what it objected to gives the reader nothing to act on.
21
+ *
22
+ * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.
23
+ */
24
+ let ChecklistRefusalRenderer = class ChecklistRefusalRenderer {
25
+ /**
26
+ * One refusal, in the terms its own verdict demands.
27
+ *
28
+ * `archivedPath` non-empty ⇒ the verdict has just been RETIRED (moved) there, which changes the message
29
+ * in two ways: it says where the record went, so the move does not read as data loss, and it asks for a
30
+ * FRESH verdict file rather than pointing at a path that no longer exists.
31
+ */
32
+ render(req, verdict, archivedPath = '') {
33
+ if (verdict.status === review_json_data_1.CK_UNAUTHORIZED)
34
+ return this.unauthorizedOverride(req, verdict, archivedPath);
35
+ const finding = `${verdict.detail.split('\n').join('\n ')}\n`;
36
+ const head = `Checklist "${req.id}" FAILED review (status:"${review_json_data_1.VERDICT_RED}"). The reviewer (${req.subagent}) wrote:\n ` + finding;
37
+ if (archivedPath === '') {
38
+ return head +
39
+ ` Fix it, then re-run. To ship it UNFIXED instead, a HUMAN must authorize it — see ` +
40
+ `${this.authorizeHint(req.id)}`;
41
+ }
42
+ // Re-spawning is the LAST thing said, and only after the finding, because an instruction to spawn a
43
+ // subagent is the one line an AI acts on first — see refusedChecklists for what that cost.
44
+ return head +
45
+ ` That verdict has been RETIRED to ${archivedPath} (audit only — it is not a live verdict).\n` +
46
+ ` A FRESH ${this.checklistFileName(req.id)} is now required. Fix the finding first, then have the ` +
47
+ `"${req.subagent}" subagent review again and write a new verdict.\n` +
48
+ ` To ship it UNFIXED instead: ${this.authorizeHint(req.id)}`;
49
+ }
50
+ /**
51
+ * The refusal for an override NOBODY AUTHORIZED — a reviewer went red and then wrote its own
52
+ * ship-anyway justification, with no signed human approval on the branch behind it.
53
+ *
54
+ * Worded as a completely different event from a plain refusal, because it asks for a different action.
55
+ * A refusal says FIX THE FINDING. This says: the decision to accept the finding is not yours to make, so
56
+ * STOP and ask a person — and it names the command that person runs. Telling this reader to fix the code
57
+ * would send them to re-do work somebody may already have decided to accept, and telling them to "get an
58
+ * override" is what they already, wrongly, did.
59
+ */
60
+ unauthorizedOverride(req, verdict, archivedPath) {
61
+ const claimed = `${verdict.detail.split('\n').join('\n ')}\n`;
62
+ const retired = archivedPath === '' ? ''
63
+ : ` That verdict has been RETIRED to ${archivedPath} (audit only — it is not a live verdict).\n`;
64
+ return (`Checklist "${req.id}" is RED and carries an override that NO HUMAN AUTHORIZED. The override written ` +
65
+ `into ${this.checklistFileName(req.id)} was:\n ` + claimed + retired +
66
+ ` An override is only honoured while a signed human approval for "${req.id}" verifies on this ` +
67
+ `branch. Writing the field yourself is an agent authorizing itself, which is exactly what this gate ` +
68
+ `stops — and no message, ticket comment or relayed quote counts either.\n` +
69
+ ` ${this.authorizeHint(req.id)}`);
70
+ }
71
+ /**
72
+ * The ONE sentence that names the human-authorization channel, so every refusal says it identically.
73
+ *
74
+ * Deliberately addressed to the AGENT about what to ASK FOR, not about what to run: `wp-authorize` reads
75
+ * from `/dev/tty` and an agent has none, so an agent that tries it hangs or is denied. `wp-check-auth` is
76
+ * the half an agent runs, and it is named second so there is something to do once the human is done.
77
+ */
78
+ authorizeHint(checklistId) {
79
+ return (`ASK THE HUMAN to run, in their own terminal: pnpm wp-authorize --checklist ${checklistId}\n` +
80
+ ` (it prompts THEM, at a tty, for what they are approving and why — you cannot run it and must ` +
81
+ `not try). Then verify it yourself with: pnpm wp-check-auth --checklist ${checklistId}`);
82
+ }
83
+ /** The verdict file a checklist id maps to. Duplicated nowhere — ReviewJsonService asks this class. */
84
+ checklistFileName(checklistId) {
85
+ return `review-${checklistId}.json`;
86
+ }
87
+ };
88
+ exports.ChecklistRefusalRenderer = ChecklistRefusalRenderer;
89
+ exports.ChecklistRefusalRenderer = ChecklistRefusalRenderer = tslib_1.__decorate([
90
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
91
+ ], ChecklistRefusalRenderer);
92
+ //# sourceMappingURL=checklist-refusal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checklist-refusal.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/checklist-refusal.ts"],"names":[],"mappings":";;;;AAAA,yCAA2D;AAC3D,yDAAuG;AAEvG;;;;;;;;;;;;;;;;GAgBG;AAEI,IAAM,wBAAwB,GAA9B,MAAM,wBAAwB;IACjC;;;;;;OAMG;IACH,MAAM,CAAC,GAAsB,EAAE,OAAyB,EAAE,YAAY,GAAG,EAAE;QACvE,IAAI,OAAO,CAAC,MAAM,KAAK,kCAAe;YAAE,OAAO,IAAI,CAAC,oBAAoB,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC;QACrG,MAAM,OAAO,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QACnE,MAAM,IAAI,GAAG,cAAc,GAAG,CAAC,EAAE,4BAA4B,8BAAW,qBAAqB,GAAG,CAAC,QAAQ,kBAAkB,GAAG,OAAO,CAAC;QACtI,IAAI,YAAY,KAAK,EAAE,EAAE,CAAC;YACtB,OAAO,IAAI;gBACP,yFAAyF;gBACzF,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;QACxC,CAAC;QACD,oGAAoG;QACpG,2FAA2F;QAC3F,OAAO,IAAI;YACP,0CAA0C,YAAY,6CAA6C;YACnG,iBAAiB,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,yDAAyD;YACxG,IAAI,GAAG,CAAC,QAAQ,oDAAoD;YACpE,qCAAqC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;IAC1E,CAAC;IAED;;;;;;;;;OASG;IACK,oBAAoB,CAAC,GAAsB,EAAE,OAAyB,EAAE,YAAoB;QAChG,MAAM,OAAO,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QACnE,MAAM,OAAO,GAAG,YAAY,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE;YACpC,CAAC,CAAC,0CAA0C,YAAY,6CAA6C,CAAC;QAC1G,OAAO,CACH,cAAc,GAAG,CAAC,EAAE,kFAAkF;YACtG,QAAQ,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,eAAe,GAAG,OAAO,GAAG,OAAO;YACzE,yEAAyE,GAAG,CAAC,EAAE,qBAAqB;YACpG,qGAAqG;YACrG,0EAA0E;YAC1E,SAAS,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CACxC,CAAC;IACN,CAAC;IAED;;;;;;OAMG;IACK,aAAa,CAAC,WAAmB;QACrC,OAAO,CACH,+EAA+E,WAAW,IAAI;YAC9F,qGAAqG;YACrG,2EAA2E,WAAW,EAAE,CAC3F,CAAC;IACN,CAAC;IAED,uGAAuG;IACvG,iBAAiB,CAAC,WAAmB;QACjC,OAAO,UAAU,WAAW,OAAO,CAAC;IACxC,CAAC;CACJ,CAAA;AArEY,4DAAwB;mCAAxB,wBAAwB;IADpC,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,wBAAwB,CAqEpC","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport { CK_UNAUTHORIZED, ChecklistVerdict, RequiredChecklist, VERDICT_RED } from './review-json-data';\n\n/**\n * THE renderer for \"this reviewer said no\" — one wording for every refusal, wherever it surfaces.\n *\n * It is its own class for the reason {@link ReviewerVerdictGate} is: this is the text an AI ACTS ON, so it\n * is the piece most worth asserting on directly, and `review-json.ts` was already at its file-size limit.\n * The behaviour is unchanged by the move — `ReviewJsonService.refusalError` delegates here, so there is\n * still exactly ONE implementation and no call site has two spellings to choose between.\n *\n * The text was previously inlined inside review.json validation, reachable only through that path, while\n * the command layer refused earlier with its own generic message. Two messages for one event is how the\n * useful one became unreachable.\n *\n * It always quotes the reviewer's own words verbatim: the finding is the whole point, and an error that\n * names a checklist without saying what it objected to gives the reader nothing to act on.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ChecklistRefusalRenderer {\n /**\n * One refusal, in the terms its own verdict demands.\n *\n * `archivedPath` non-empty ⇒ the verdict has just been RETIRED (moved) there, which changes the message\n * in two ways: it says where the record went, so the move does not read as data loss, and it asks for a\n * FRESH verdict file rather than pointing at a path that no longer exists.\n */\n render(req: RequiredChecklist, verdict: ChecklistVerdict, archivedPath = ''): string {\n if (verdict.status === CK_UNAUTHORIZED) return this.unauthorizedOverride(req, verdict, archivedPath);\n const finding = `${verdict.detail.split('\\n').join('\\n ')}\\n`;\n const head = `Checklist \"${req.id}\" FAILED review (status:\"${VERDICT_RED}\"). The reviewer (${req.subagent}) wrote:\\n ` + finding;\n if (archivedPath === '') {\n return head +\n ` Fix it, then re-run. To ship it UNFIXED instead, a HUMAN must authorize it — see ` +\n `${this.authorizeHint(req.id)}`;\n }\n // Re-spawning is the LAST thing said, and only after the finding, because an instruction to spawn a\n // subagent is the one line an AI acts on first — see refusedChecklists for what that cost.\n return head +\n ` That verdict has been RETIRED to ${archivedPath} (audit only — it is not a live verdict).\\n` +\n ` A FRESH ${this.checklistFileName(req.id)} is now required. Fix the finding first, then have the ` +\n `\"${req.subagent}\" subagent review again and write a new verdict.\\n` +\n ` To ship it UNFIXED instead: ${this.authorizeHint(req.id)}`;\n }\n\n /**\n * The refusal for an override NOBODY AUTHORIZED — a reviewer went red and then wrote its own\n * ship-anyway justification, with no signed human approval on the branch behind it.\n *\n * Worded as a completely different event from a plain refusal, because it asks for a different action.\n * A refusal says FIX THE FINDING. This says: the decision to accept the finding is not yours to make, so\n * STOP and ask a person — and it names the command that person runs. Telling this reader to fix the code\n * would send them to re-do work somebody may already have decided to accept, and telling them to \"get an\n * override\" is what they already, wrongly, did.\n */\n private unauthorizedOverride(req: RequiredChecklist, verdict: ChecklistVerdict, archivedPath: string): string {\n const claimed = `${verdict.detail.split('\\n').join('\\n ')}\\n`;\n const retired = archivedPath === '' ? ''\n : ` That verdict has been RETIRED to ${archivedPath} (audit only — it is not a live verdict).\\n`;\n return (\n `Checklist \"${req.id}\" is RED and carries an override that NO HUMAN AUTHORIZED. The override written ` +\n `into ${this.checklistFileName(req.id)} was:\\n ` + claimed + retired +\n ` An override is only honoured while a signed human approval for \"${req.id}\" verifies on this ` +\n `branch. Writing the field yourself is an agent authorizing itself, which is exactly what this gate ` +\n `stops — and no message, ticket comment or relayed quote counts either.\\n` +\n ` ${this.authorizeHint(req.id)}`\n );\n }\n\n /**\n * The ONE sentence that names the human-authorization channel, so every refusal says it identically.\n *\n * Deliberately addressed to the AGENT about what to ASK FOR, not about what to run: `wp-authorize` reads\n * from `/dev/tty` and an agent has none, so an agent that tries it hangs or is denied. `wp-check-auth` is\n * the half an agent runs, and it is named second so there is something to do once the human is done.\n */\n private authorizeHint(checklistId: string): string {\n return (\n `ASK THE HUMAN to run, in their own terminal: pnpm wp-authorize --checklist ${checklistId}\\n` +\n ` (it prompts THEM, at a tty, for what they are approving and why — you cannot run it and must ` +\n `not try). Then verify it yourself with: pnpm wp-check-auth --checklist ${checklistId}`\n );\n }\n\n /** The verdict file a checklist id maps to. Duplicated nowhere — ReviewJsonService asks this class. */\n checklistFileName(checklistId: string): string {\n return `review-${checklistId}.json`;\n }\n}\n"]}
@@ -28,5 +28,21 @@ export declare function isPathExcluded(relPath: string, excludePaths: readonly s
28
28
  *
29
29
  * Paths are normalized to forward slashes so Windows backslashes match too. Empty `patterns` matches
30
30
  * nothing, so an unconfigured list is inert rather than universal.
31
+ *
32
+ * DOTFILE PATHS COUNT (`{ dot: true }`). minimatch's default hides any path segment starting with `.`
33
+ * from a wildcard, so `**` does not match `.github/workflows/deploy.yml` and `.claude/**` does not match
34
+ * `.claude/agents/x.md`. For a repo-path classifier that default is simply wrong: `.github/`, `.claude/`
35
+ * and `.webpieces/` are ordinary, frequently-changed directories here, and a pattern author writing
36
+ * `**` means all of it. It was also a live hazard — a human authorization scoped to a diff that includes
37
+ * a dotfile path would have silently failed to cover it, so the approval would die for a reason nobody
38
+ * could see. Shell-style dot-hiding protects against globbing up `.` and `..`, which is not a hazard any
39
+ * caller here has.
40
+ *
41
+ * The three consumers OUTSIDE the authorization path are named here on purpose, because the widening is
42
+ * silent for them: `build-artifact-gate` (stale generated artifacts), `api-scanner.externalApiPaths`
43
+ * (project classification) and `no-custom-css-scope.allowGlobs` (an EXEMPTION list, so wider = more
44
+ * exempt). None configures a dot-path today, and for all three "the pattern author meant the whole
45
+ * subtree" is the reading they already assume — but a future dot-path entry now matches where it
46
+ * silently did not, and that is a deliberate decision recorded here rather than an accident.
31
47
  */
32
48
  export declare function matchesAnyGlob(relPath: string, patterns: readonly string[]): boolean;
@@ -45,14 +45,30 @@ function isPathExcluded(relPath, excludePaths) {
45
45
  *
46
46
  * Paths are normalized to forward slashes so Windows backslashes match too. Empty `patterns` matches
47
47
  * nothing, so an unconfigured list is inert rather than universal.
48
+ *
49
+ * DOTFILE PATHS COUNT (`{ dot: true }`). minimatch's default hides any path segment starting with `.`
50
+ * from a wildcard, so `**` does not match `.github/workflows/deploy.yml` and `.claude/**` does not match
51
+ * `.claude/agents/x.md`. For a repo-path classifier that default is simply wrong: `.github/`, `.claude/`
52
+ * and `.webpieces/` are ordinary, frequently-changed directories here, and a pattern author writing
53
+ * `**` means all of it. It was also a live hazard — a human authorization scoped to a diff that includes
54
+ * a dotfile path would have silently failed to cover it, so the approval would die for a reason nobody
55
+ * could see. Shell-style dot-hiding protects against globbing up `.` and `..`, which is not a hazard any
56
+ * caller here has.
57
+ *
58
+ * The three consumers OUTSIDE the authorization path are named here on purpose, because the widening is
59
+ * silent for them: `build-artifact-gate` (stale generated artifacts), `api-scanner.externalApiPaths`
60
+ * (project classification) and `no-custom-css-scope.allowGlobs` (an EXEMPTION list, so wider = more
61
+ * exempt). None configures a dot-path today, and for all three "the pattern author meant the whole
62
+ * subtree" is the reading they already assume — but a future dot-path entry now matches where it
63
+ * silently did not, and that is a deliberate decision recorded here rather than an accident.
48
64
  */
49
65
  // webpieces-disable no-function-outside-class -- pure path predicate, sibling of isPathExcluded above
50
66
  function matchesAnyGlob(relPath, patterns) {
51
67
  const norm = relPath.replace(/\\/g, '/');
52
68
  for (const pattern of patterns) {
53
- if ((0, minimatch_1.minimatch)(norm, pattern))
69
+ if ((0, minimatch_1.minimatch)(norm, pattern, { dot: true }))
54
70
  return true;
55
- if ((0, minimatch_1.minimatch)(norm, `${pattern}/**`))
71
+ if ((0, minimatch_1.minimatch)(norm, `${pattern}/**`, { dot: true }))
56
72
  return true;
57
73
  }
58
74
  return false;
@@ -1 +1 @@
1
- {"version":3,"file":"exclude-paths.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/exclude-paths.ts"],"names":[],"mappings":";;AAoBA,wCASC;AAeD,wCAOC;AAnDD,yCAAsC;AAEtC;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAgB,cAAc,CAAC,OAAe,EAAE,YAA+B;IAC3E,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACjC,KAAK,MAAM,OAAO,IAAI,YAAY,EAAE,CAAC;QACjC,IAAI,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QAC5C,IAAI,IAAA,qBAAS,EAAC,IAAI,EAAE,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QAC1C,IAAI,IAAA,qBAAS,EAAC,IAAI,EAAE,GAAG,OAAO,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;IACtD,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,sGAAsG;AACtG,SAAgB,cAAc,CAAC,OAAe,EAAE,QAA2B;IACvE,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACzC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC7B,IAAI,IAAA,qBAAS,EAAC,IAAI,EAAE,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QAC1C,IAAI,IAAA,qBAAS,EAAC,IAAI,EAAE,GAAG,OAAO,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;IACtD,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC","sourcesContent":["import { minimatch } from 'minimatch';\n\n/**\n * Holistic exclusion check shared by validate-ts-in-src (Layer 1 + Layer 2)\n * and the file-location AI-hook rule, so the two implementations can never\n * drift apart again.\n *\n * `relPath` is a workspace-relative path (e.g.\n * \"libraries/foo/codegen.ts\"). An entry in `excludePaths` matches when ANY\n * of the following hold:\n *\n * 1. Bare directory/segment name appearing anywhere in the path. This is\n * the historical behavior and keeps entries like \"node_modules\",\n * \"dist\", \"scripts\", \"architecture\" working at any depth.\n * 2. A glob matched against the full relative path, e.g. \"**\\/*.d.ts\" or\n * \"**\\/codegen.ts\".\n * 3. A directory-prefix glob, e.g. \"libraries/apis\" -> \"libraries/apis/**\".\n *\n * Paths are normalized to forward slashes so Windows backslashes match too.\n */\nexport function isPathExcluded(relPath: string, excludePaths: readonly string[]): boolean {\n const norm = relPath.replace(/\\\\/g, '/');\n const segments = norm.split('/');\n for (const pattern of excludePaths) {\n if (segments.includes(pattern)) return true;\n if (minimatch(norm, pattern)) return true;\n if (minimatch(norm, `${pattern}/**`)) return true;\n }\n return false;\n}\n\n/**\n * Does `relPath` match any of `patterns`, as a glob or as a directory prefix?\n *\n * The strict sibling of {@link isPathExcluded}: it drops rule 1 (bare segment name matching anywhere\n * in the path), which is right for an opt-out exclusion list but wrong for anything that CLASSIFIES\n * a path — a pattern like \"external\" would otherwise claim every path containing that segment at any\n * depth. Used for config lists that positively identify a set of projects (e.g.\n * `runtime-architecture.externalApiPaths`).\n *\n * Paths are normalized to forward slashes so Windows backslashes match too. Empty `patterns` matches\n * nothing, so an unconfigured list is inert rather than universal.\n */\n// webpieces-disable no-function-outside-class -- pure path predicate, sibling of isPathExcluded above\nexport function matchesAnyGlob(relPath: string, patterns: readonly string[]): boolean {\n const norm = relPath.replace(/\\\\/g, '/');\n for (const pattern of patterns) {\n if (minimatch(norm, pattern)) return true;\n if (minimatch(norm, `${pattern}/**`)) return true;\n }\n return false;\n}\n"]}
1
+ {"version":3,"file":"exclude-paths.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/exclude-paths.ts"],"names":[],"mappings":";;AAoBA,wCASC;AA+BD,wCAOC;AAnED,yCAAsC;AAEtC;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAgB,cAAc,CAAC,OAAe,EAAE,YAA+B;IAC3E,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACjC,KAAK,MAAM,OAAO,IAAI,YAAY,EAAE,CAAC;QACjC,IAAI,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QAC5C,IAAI,IAAA,qBAAS,EAAC,IAAI,EAAE,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QAC1C,IAAI,IAAA,qBAAS,EAAC,IAAI,EAAE,GAAG,OAAO,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;IACtD,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,sGAAsG;AACtG,SAAgB,cAAc,CAAC,OAAe,EAAE,QAA2B;IACvE,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACzC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC7B,IAAI,IAAA,qBAAS,EAAC,IAAI,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;YAAE,OAAO,IAAI,CAAC;QACzD,IAAI,IAAA,qBAAS,EAAC,IAAI,EAAE,GAAG,OAAO,KAAK,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;YAAE,OAAO,IAAI,CAAC;IACrE,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC","sourcesContent":["import { minimatch } from 'minimatch';\n\n/**\n * Holistic exclusion check shared by validate-ts-in-src (Layer 1 + Layer 2)\n * and the file-location AI-hook rule, so the two implementations can never\n * drift apart again.\n *\n * `relPath` is a workspace-relative path (e.g.\n * \"libraries/foo/codegen.ts\"). An entry in `excludePaths` matches when ANY\n * of the following hold:\n *\n * 1. Bare directory/segment name appearing anywhere in the path. This is\n * the historical behavior and keeps entries like \"node_modules\",\n * \"dist\", \"scripts\", \"architecture\" working at any depth.\n * 2. A glob matched against the full relative path, e.g. \"**\\/*.d.ts\" or\n * \"**\\/codegen.ts\".\n * 3. A directory-prefix glob, e.g. \"libraries/apis\" -> \"libraries/apis/**\".\n *\n * Paths are normalized to forward slashes so Windows backslashes match too.\n */\nexport function isPathExcluded(relPath: string, excludePaths: readonly string[]): boolean {\n const norm = relPath.replace(/\\\\/g, '/');\n const segments = norm.split('/');\n for (const pattern of excludePaths) {\n if (segments.includes(pattern)) return true;\n if (minimatch(norm, pattern)) return true;\n if (minimatch(norm, `${pattern}/**`)) return true;\n }\n return false;\n}\n\n/**\n * Does `relPath` match any of `patterns`, as a glob or as a directory prefix?\n *\n * The strict sibling of {@link isPathExcluded}: it drops rule 1 (bare segment name matching anywhere\n * in the path), which is right for an opt-out exclusion list but wrong for anything that CLASSIFIES\n * a path — a pattern like \"external\" would otherwise claim every path containing that segment at any\n * depth. Used for config lists that positively identify a set of projects (e.g.\n * `runtime-architecture.externalApiPaths`).\n *\n * Paths are normalized to forward slashes so Windows backslashes match too. Empty `patterns` matches\n * nothing, so an unconfigured list is inert rather than universal.\n *\n * DOTFILE PATHS COUNT (`{ dot: true }`). minimatch's default hides any path segment starting with `.`\n * from a wildcard, so `**` does not match `.github/workflows/deploy.yml` and `.claude/**` does not match\n * `.claude/agents/x.md`. For a repo-path classifier that default is simply wrong: `.github/`, `.claude/`\n * and `.webpieces/` are ordinary, frequently-changed directories here, and a pattern author writing\n * `**` means all of it. It was also a live hazard — a human authorization scoped to a diff that includes\n * a dotfile path would have silently failed to cover it, so the approval would die for a reason nobody\n * could see. Shell-style dot-hiding protects against globbing up `.` and `..`, which is not a hazard any\n * caller here has.\n *\n * The three consumers OUTSIDE the authorization path are named here on purpose, because the widening is\n * silent for them: `build-artifact-gate` (stale generated artifacts), `api-scanner.externalApiPaths`\n * (project classification) and `no-custom-css-scope.allowGlobs` (an EXEMPTION list, so wider = more\n * exempt). None configures a dot-path today, and for all three \"the pattern author meant the whole\n * subtree\" is the reading they already assume — but a future dot-path entry now matches where it\n * silently did not, and that is a deliberate decision recorded here rather than an accident.\n */\n// webpieces-disable no-function-outside-class -- pure path predicate, sibling of isPathExcluded above\nexport function matchesAnyGlob(relPath: string, patterns: readonly string[]): boolean {\n const norm = relPath.replace(/\\\\/g, '/');\n for (const pattern of patterns) {\n if (minimatch(norm, pattern, { dot: true })) return true;\n if (minimatch(norm, `${pattern}/**`, { dot: true })) return true;\n }\n return false;\n}\n"]}
@@ -0,0 +1,203 @@
1
+ import { DotWebpieces } from './state-dir';
2
+ export declare const AUTHORIZATIONS_DIR = "authorizations";
3
+ export declare const DEFAULT_APPROVAL_HOURS = 4;
4
+ /**
5
+ * ONE human approval of ONE checklist's override, on ONE branch. Data-only (per CLAUDE.md).
6
+ *
7
+ * `approves` is prose the human typed at the tty in their own words, and it is the POINT of the record:
8
+ * `wp-check-auth` prints it, so a reviewer can judge whether the approval actually covers the thing it is
9
+ * being applied to — not merely that *an* approval exists on this branch.
10
+ *
11
+ * `hmac` is `HMAC-SHA256(prGate.gateSalt, canonical payload)` over every other field plus the branch. It
12
+ * is what makes the approval VERIFIABLE by an agent that cannot MINT one: the agent runs `wp-check-auth`,
13
+ * which recomputes the HMAC, rather than believing a claim relayed to it in a message.
14
+ */
15
+ export declare class HumanApproval {
16
+ checklist: string;
17
+ gate: string;
18
+ approves: string;
19
+ scopePaths: string[];
20
+ forkPoint: string;
21
+ issuedAt: string;
22
+ expiresAt: string;
23
+ hmac: string;
24
+ constructor(checklist: string, gate: string, approves: string, scopePaths: string[], forkPoint: string, issuedAt: string, expiresAt: string, hmac?: string);
25
+ }
26
+ /**
27
+ * The append-only authorization file for ONE branch: N approvals, each naming what it approves. Data-only.
28
+ *
29
+ * The unit is the BRANCH, not the gate, because one run of work routinely needs more than one override and
30
+ * they arrive at different times. One file per gate would mean the human re-answering the same "where does
31
+ * this go?" question on every approval.
32
+ */
33
+ export declare class AuthorizationFile {
34
+ branch: string;
35
+ approvals: HumanApproval[];
36
+ /**
37
+ * '' when the file was absent or read cleanly. Non-empty = it EXISTS but could not be parsed, and the
38
+ * reason.
39
+ *
40
+ * Carried rather than swallowed because "no approvals" and "approvals I could not read" are different
41
+ * facts and the second one is actionable: a corrupt file means the human's approval is sitting on disk
42
+ * unreadable, and the fix is to re-authorize rather than to wonder why nothing was found. `verifiedFor`
43
+ * turns this into a `rejected` line, which `wp-check-auth` already prints.
44
+ */
45
+ unreadable: string;
46
+ constructor(branch?: string, approvals?: HumanApproval[], unreadable?: string);
47
+ }
48
+ /**
49
+ * The git facts an approval is verified AGAINST, gathered once by the caller. Data-only.
50
+ *
51
+ * A data class rather than four parameters because the four must describe ONE branch state: a `forkPoint`
52
+ * resolved from one tree checked against a `changedFiles` gathered from another is a verification that
53
+ * means nothing, and separate parameters are how that happens.
54
+ */
55
+ export declare class AuthorizationContext {
56
+ branch: string;
57
+ forkPoint: string;
58
+ changedFiles: string[];
59
+ now: Date;
60
+ constructor(branch: string, forkPoint: string, changedFiles?: string[], now?: Date);
61
+ }
62
+ /**
63
+ * The verdict on ONE approval. `ok` false ⇒ `reason` says WHICH of the four bindings failed, in the
64
+ * human's terms, because that is the sentence the agent has to relay back to them. Data-only.
65
+ */
66
+ export declare class AuthorizationCheck {
67
+ ok: boolean;
68
+ reason: string;
69
+ approval: HumanApproval | null;
70
+ constructor(ok: boolean, reason: string, approval: HumanApproval | null);
71
+ }
72
+ /**
73
+ * What a branch is actually authorized to override RIGHT NOW: checklist id → the human's `approves` prose.
74
+ * Data-only, with accessors so every consumer asks the question one way.
75
+ *
76
+ * `rejected` carries the approvals that were found but did NOT verify, one rendered line each. They are
77
+ * kept rather than dropped because "there is an approval on this branch, and here is why it no longer
78
+ * counts" is a completely different message from "nobody has authorized anything" — the first needs a
79
+ * re-authorization, the second needs a human at all — and an empty map cannot tell them apart.
80
+ */
81
+ export declare class AuthorizedOverrides {
82
+ proseById: Map<string, string>;
83
+ rejected: string[];
84
+ constructor(proseById?: Map<string, string>, rejected?: string[]);
85
+ has(checklistId: string): boolean;
86
+ /** The human's own words for `checklistId`, or '' when it is not authorized. */
87
+ proseFor(checklistId: string): string;
88
+ }
89
+ /**
90
+ * Mints (for a HUMAN at a tty) and verifies (for ANYONE, agents included) the human-authorization records
91
+ * that are the ONLY channel by which a review checklist's override may be granted.
92
+ *
93
+ * ─── The problem ───────────────────────────────────────────────────────────────────────────────────────
94
+ * A required checklist goes red, the human authorizes the partial scope, and there is NO channel by which
95
+ * the subagent doing the work can know that. Every channel available before this carried a CLAIM of
96
+ * authorization and never EVIDENCE of it: a coordinator relaying the human's words is unverifiable by
97
+ * construction (and correctly refused — that refusal is the shape a prompt injection exploits); a ticket
98
+ * comment can be written by an agent holding the same MCP; and the `override` field in review-<id>.json is
99
+ * the agent authorizing itself.
100
+ *
101
+ * So the property this class buys is exactly one sentence: **an agent can VERIFY an authorization it
102
+ * cannot MINT.** `wp-authorize` reads the approval from `/dev/tty`, which an agent's Bash tool has no way
103
+ * to answer; `wp-check-auth` recomputes the HMAC and is read-only, so agents run it freely.
104
+ *
105
+ * ─── Bound to SCOPE, never to a diff sha ───────────────────────────────────────────────────────────────
106
+ * An approval bound to the head-commit diff would be void on the next commit, so the human would
107
+ * re-authorize on every push and nobody would use it. Each approval binds to WHAT WAS APPROVED instead:
108
+ * • `scopePaths` — the globs the diff touched when approved. A "terraform only" approval is void the
109
+ * moment app files appear, which is the abuse actually worth stopping.
110
+ * • `forkPoint` — the merge-base when approved. Survives new commits; dies if the branch is restarted.
111
+ * • `expiresAt` — hours, not days.
112
+ * Edits inside the approved scope keep working; widening it does not. That is what the human means when
113
+ * they say "yes, ship the terraform half".
114
+ *
115
+ * ─── Where the file lives, and why it is SHARED rather than per-worktree ───────────────────────────────
116
+ * `dotWebpieces.shared()/authorizations/<branch-slug>.json`, keyed by BRANCH. Not `local()`: the human
117
+ * routinely types `wp-authorize` in the primary clone while the agent works in a linked worktree, and
118
+ * under `local()` those are two different files — the approval would be minted somewhere the agent never
119
+ * looks, which is the same stall this feature exists to end. The branch key is what makes sharing safe:
120
+ * an approval names its branch inside the SIGNED payload, so a file copied to another branch does not
121
+ * verify there.
122
+ *
123
+ * It is never committed (`.webpieces/` is gitignored in full). A committed authorization would travel to
124
+ * branches nobody approved.
125
+ *
126
+ * ─── Honest limits — do not oversell this ─────────────────────────────────────────────────────────────
127
+ * The agent runs as the SAME OS USER as the human, and the HMAC key is `prGate.gateSalt`, which lives in a
128
+ * committed file agents read routinely. Nothing here is cryptographically airtight against a determined
129
+ * model; the real enforcement is the tty affordance plus the harness deny rule. That is fine, because the
130
+ * problem being solved is agents drifting, guessing, or being confused by relays — not an adversarial
131
+ * model. Moving the key to `~/.webpieces/authorize.key` is a one-line change to `sign()` if the threat
132
+ * model ever changes; nothing else in the design moves.
133
+ *
134
+ * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.
135
+ */
136
+ export declare class HumanAuthorizationService {
137
+ private readonly dotDir;
138
+ constructor(dotDir?: DotWebpieces);
139
+ /** The repo-wide authorizations directory. See the class docstring for why `shared()` and not `local()`. */
140
+ dirFor(repoRoot: string): string;
141
+ /**
142
+ * A filesystem-safe leaf for a branch name. Slashes and anything exotic collapse to `-`, so
143
+ * `dean/one-2779-grants` becomes `dean-one-2779-grants`.
144
+ *
145
+ * A collision between two slugs is harmless: the branch is inside the SIGNED payload, so an approval
146
+ * that landed in a colliding file fails `verify` on the branch check rather than leaking across.
147
+ */
148
+ slugFor(branch: string): string;
149
+ pathFor(repoRoot: string, branch: string): string;
150
+ /**
151
+ * The exact bytes that get signed: a version tag, the branch, and every field of the approval EXCEPT
152
+ * the hmac, in a fixed order with `scopePaths` sorted.
153
+ *
154
+ * Fixed order and sorting are load-bearing — `JSON.stringify` over a re-parsed object preserves
155
+ * insertion order, so a round-tripped approval whose keys arrived in a different order would compute a
156
+ * different payload and fail to verify a signature that is perfectly good.
157
+ */
158
+ canonicalPayload(branch: string, approval: HumanApproval): string;
159
+ /** `HMAC-SHA256(salt, canonical payload)`. '' for an empty salt — the caller treats '' as "not configured". */
160
+ sign(branch: string, approval: HumanApproval, salt: string): string;
161
+ /** The same approval, carrying its signature. Never mutates the input. */
162
+ signed(branch: string, approval: HumanApproval, salt: string): HumanApproval;
163
+ /**
164
+ * Every approval recorded for `branch`, signature UNCHECKED. An absent or unreadable file is an EMPTY
165
+ * file, never a throw: a corrupt authorization must degrade to "nothing is authorized", which is the
166
+ * safe direction, and a branch that cannot open its own authorization file must still be able to run
167
+ * the gate and be told to go get one.
168
+ */
169
+ load(repoRoot: string, branch: string): AuthorizationFile;
170
+ private toApproval;
171
+ private str;
172
+ private strList;
173
+ /**
174
+ * APPEND one signed approval to the branch's file and return the path written. Append, never replace:
175
+ * a run of work needs several overrides at different times, and each one is its own record of intent.
176
+ */
177
+ append(repoRoot: string, branch: string, approval: HumanApproval, salt: string): string;
178
+ /**
179
+ * Verify ONE approval against the branch state, in the order a reader wants to hear it: signature,
180
+ * expiry, fork point, scope. Signature first — an unsigned entry's other fields are not evidence of
181
+ * anything, and reporting "expired" for a forged record would be answering the wrong question.
182
+ */
183
+ verify(ctx: AuthorizationContext, approval: HumanApproval, salt: string): AuthorizationCheck;
184
+ private fail;
185
+ private expired;
186
+ /**
187
+ * The changed files NOT covered by `scopePaths`. An approval with an EMPTY `scopePaths` covers nothing
188
+ * and is therefore always out of scope when anything changed — `wp-authorize` never mints one, and the
189
+ * alternative reading ("empty means everything") is precisely the widening-by-absence that makes the
190
+ * permissive path the shortest thing to type.
191
+ */
192
+ private outsideScope;
193
+ /**
194
+ * THE question every consumer asks: what is this branch authorized to override right now? Verifies every
195
+ * recorded approval and returns the ones that hold, plus a rendered line per one that does not.
196
+ *
197
+ * Later approvals win for the same checklist — the human appended it because they meant to say something
198
+ * newer, and the file is append-only precisely so the earlier one stays readable as history.
199
+ */
200
+ verifiedFor(repoRoot: string, ctx: AuthorizationContext, salt: string): AuthorizedOverrides;
201
+ /** An expiry `hours` from `issuedAt`, as ISO — the one place the TTL arithmetic lives. */
202
+ expiryFrom(issuedAt: Date, hours: number): string;
203
+ }