@pareto4/pp-core 1.5.4 → 1.6.0
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/.claude-plugin/plugin.json +1 -1
- package/commands/pp-init.md +36 -0
- package/commands/pp-ns-project.md +2 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/pp-core/bin/lib/capability-registry.cjs +135 -2
- package/pp-core/bin/lib/check-command-router.cjs +183 -1
- package/pp-core/bin/lib/clusters.cjs +1 -0
- package/pp-core/bin/lib/command-aliases.cjs +7 -0
- package/pp-core/bin/lib/io.cjs +2 -0
- package/pp-core/bin/lib/platform-bindings.cjs +336 -0
- package/pp-core/bin/lib/profile-output.cjs +216 -1
- package/pp-core/bin/pp-tools.cjs +40 -3
- package/pp-core/workflows/discovery-sprint.md +27 -10
- package/pp-core/workflows/execute-phase/steps/client-context-gate.md +37 -0
- package/pp-core/workflows/execute-phase.md +4 -0
- package/pp-core/workflows/init.md +530 -0
- package/pp-core/workflows/new-project.md +54 -1
- package/pp-core/workflows/ship.md +24 -1
- package/pp-core/workflows/verify-work.md +1 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pp:init
|
|
3
|
+
description: Initialize a project — git init, remote wiring, minimal metadata, then hand off
|
|
4
|
+
argument-hint: "[--text]"
|
|
5
|
+
allowed-tools:
|
|
6
|
+
- Read
|
|
7
|
+
- Bash
|
|
8
|
+
- Write
|
|
9
|
+
- AskUserQuestion
|
|
10
|
+
requires: [config]
|
|
11
|
+
---
|
|
12
|
+
<runtime_note>
|
|
13
|
+
**Copilot (VS Code):** Use `vscode_askquestions` wherever this workflow calls `AskUserQuestion`. They are equivalent — `vscode_askquestions` is the VS Code Copilot implementation of the same interactive question API.
|
|
14
|
+
</runtime_note>
|
|
15
|
+
|
|
16
|
+
<objective>
|
|
17
|
+
Universal project entry point. Takes any directory — empty or with existing code — from zero
|
|
18
|
+
to: git-initialized, minimal identity captured, remote wired, and handed off to the next
|
|
19
|
+
workflow.
|
|
20
|
+
|
|
21
|
+
Works for every project regardless of provider. The gated GitLab provisioning path
|
|
22
|
+
(Pareto satellite repos with `.platform-bindings.json`) is added in Plan 02 and slots
|
|
23
|
+
into the structural anchor already present in the workflow.
|
|
24
|
+
|
|
25
|
+
**After this command:** You will be asked whether to continue with `/pp-discovery`
|
|
26
|
+
(client/Pareto projects) or `/pp-new-project` (greenfield planning).
|
|
27
|
+
</objective>
|
|
28
|
+
|
|
29
|
+
<execution_context>
|
|
30
|
+
@~/.claude/pp-core/workflows/init.md
|
|
31
|
+
</execution_context>
|
|
32
|
+
|
|
33
|
+
<process>
|
|
34
|
+
Execute end-to-end.
|
|
35
|
+
Preserve all workflow gates: confirmation, commits, routing.
|
|
36
|
+
</process>
|
|
@@ -5,7 +5,7 @@ argument-hint: ""
|
|
|
5
5
|
allowed-tools:
|
|
6
6
|
- Read
|
|
7
7
|
- Skill
|
|
8
|
-
requires: [new-project, new-milestone, complete-milestone, audit-milestone, milestone-summary, import, ingest-docs, profile-user, review-backlog]
|
|
8
|
+
requires: [init, new-project, new-milestone, complete-milestone, audit-milestone, milestone-summary, import, ingest-docs, profile-user, review-backlog]
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
Route to the appropriate project / milestone skill based on the user's intent.
|
|
@@ -14,6 +14,7 @@ inline as part of `pp-audit-milestone`'s output.
|
|
|
14
14
|
|
|
15
15
|
| User wants | Invoke |
|
|
16
16
|
|---|---|
|
|
17
|
+
| Initialize a project (canonical front door) | pp-init |
|
|
17
18
|
| Start a new project | pp-new-project |
|
|
18
19
|
| Create a new milestone | pp-new-milestone |
|
|
19
20
|
| Complete the current milestone | pp-complete-milestone |
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pp-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "PP Core — a meta-prompting, context engineering, and spec-driven development system for AI coding agents. Loads pp-core's operating context into every Gemini CLI session.",
|
|
5
5
|
"contextFileName": "GEMINI.md"
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pareto4/pp-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Pareto pp-core — The Pareto Project's AI-assisted engineering framework: meta-prompting, context engineering, and spec-driven development for AI coding agents.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"pp-core": "bin/install.js",
|
|
@@ -1307,6 +1307,77 @@ const capabilities = {
|
|
|
1307
1307
|
"extendedHookEvents": []
|
|
1308
1308
|
}
|
|
1309
1309
|
},
|
|
1310
|
+
"pareto-stack": {
|
|
1311
|
+
"id": "pareto-stack",
|
|
1312
|
+
"role": "feature",
|
|
1313
|
+
"title": "Pareto Stack drift gate",
|
|
1314
|
+
"description": "Ship-time platform drift detection for Pareto-managed stacks. Inert unless pareto.enabled is true and .platform-bindings.json is present. Classifies version gaps by severity: any major-version gap classifies as major; breaking is reserved and aliased to major for now. A blocking gate halts on block status; a non-blocking gate carries below-threshold warn advisories. Syncs board state via pareto-sync-board at ship:post.",
|
|
1315
|
+
"tier": "full",
|
|
1316
|
+
"requires": [],
|
|
1317
|
+
"runtimeCompat": {
|
|
1318
|
+
"supported": [
|
|
1319
|
+
"*"
|
|
1320
|
+
],
|
|
1321
|
+
"unsupported": []
|
|
1322
|
+
},
|
|
1323
|
+
"skills": [
|
|
1324
|
+
"pareto-sync-board"
|
|
1325
|
+
],
|
|
1326
|
+
"agents": [],
|
|
1327
|
+
"activationKey": "pareto.enabled",
|
|
1328
|
+
"config": {
|
|
1329
|
+
"pareto.enabled": {
|
|
1330
|
+
"type": "boolean",
|
|
1331
|
+
"default": false,
|
|
1332
|
+
"description": "Enable the Pareto stack capability. When false, all gates and steps are inert."
|
|
1333
|
+
},
|
|
1334
|
+
"pareto.drift_block_on": {
|
|
1335
|
+
"type": "enum",
|
|
1336
|
+
"values": [
|
|
1337
|
+
"breaking",
|
|
1338
|
+
"major",
|
|
1339
|
+
"minor",
|
|
1340
|
+
"none"
|
|
1341
|
+
],
|
|
1342
|
+
"default": "major",
|
|
1343
|
+
"description": "Minimum drift severity that blocks the ship gate. breaking is reserved and aliased to major."
|
|
1344
|
+
}
|
|
1345
|
+
},
|
|
1346
|
+
"hooks": [],
|
|
1347
|
+
"steps": [
|
|
1348
|
+
{
|
|
1349
|
+
"point": "ship:post",
|
|
1350
|
+
"ref": {
|
|
1351
|
+
"skill": "pareto-sync-board"
|
|
1352
|
+
},
|
|
1353
|
+
"produces": [],
|
|
1354
|
+
"consumes": [],
|
|
1355
|
+
"when": "pareto.enabled",
|
|
1356
|
+
"onError": "skip"
|
|
1357
|
+
}
|
|
1358
|
+
],
|
|
1359
|
+
"contributions": [],
|
|
1360
|
+
"gates": [
|
|
1361
|
+
{
|
|
1362
|
+
"point": "ship:pre",
|
|
1363
|
+
"check": {
|
|
1364
|
+
"query": "ship.platform-drift"
|
|
1365
|
+
},
|
|
1366
|
+
"when": "pareto.enabled",
|
|
1367
|
+
"blocking": true,
|
|
1368
|
+
"onError": "skip"
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
"point": "ship:pre",
|
|
1372
|
+
"check": {
|
|
1373
|
+
"query": "ship.platform-drift"
|
|
1374
|
+
},
|
|
1375
|
+
"when": "pareto.enabled",
|
|
1376
|
+
"blocking": false,
|
|
1377
|
+
"onError": "skip"
|
|
1378
|
+
}
|
|
1379
|
+
]
|
|
1380
|
+
},
|
|
1310
1381
|
"pattern-mapper": {
|
|
1311
1382
|
"id": "pattern-mapper",
|
|
1312
1383
|
"role": "feature",
|
|
@@ -1561,7 +1632,7 @@ const capabilities = {
|
|
|
1561
1632
|
"into": "planner",
|
|
1562
1633
|
"fragment": {
|
|
1563
1634
|
"path": "fragments/plan-pre.md",
|
|
1564
|
-
"inline": "# Schema Push Detection Gate\n\n> Detects schema-relevant files in the phase scope and injects a mandatory `[BLOCKING]` schema push task into the plan. Prevents false-positive verification where build/types pass because TypeScript types come from config, not the live database.\n\nCheck if any files in the phase scope match schema patterns:\n\n```bash\nPHASE_SECTION=$(
|
|
1635
|
+
"inline": "# Schema Push Detection Gate\n\n> Detects schema-relevant files in the phase scope and injects a mandatory `[BLOCKING]` schema push task into the plan. Prevents false-positive verification where build/types pass because TypeScript types come from config, not the live database.\n\nCheck if any files in the phase scope match schema patterns:\n\n```bash\nPHASE_SECTION=$(pp_run query roadmap.get-phase \"${PHASE}\" --pick section 2>/dev/null)\n```\n\nScan `PHASE_SECTION`, `CONTEXT.md` (if loaded), and `RESEARCH.md` (if exists) for file paths matching these ORM patterns:\n\n| ORM | File Patterns |\n|-----|--------------|\n| Payload CMS | `src/collections/**/*.ts`, `src/globals/**/*.ts` |\n| Prisma | `prisma/schema.prisma`, `prisma/schema/*.prisma` |\n| Drizzle | `drizzle/schema.ts`, `src/db/schema.ts`, `drizzle/*.ts` |\n| Supabase | `supabase/migrations/*.sql` |\n| TypeORM | `src/entities/**/*.ts`, `src/migrations/**/*.ts` |\n\nAlso check if any existing PLAN.md files for this phase already reference these file patterns in `files_modified`.\n\n**If schema-relevant files detected:**\n\nSet `SCHEMA_PUSH_REQUIRED=true` and `SCHEMA_ORM={detected_orm}`.\n\nDetermine the push command for the detected ORM:\n\n| ORM | Push Command | Non-TTY Workaround |\n|-----|-------------|-------------------|\n| Payload CMS | `npx payload migrate` | `CI=true PAYLOAD_MIGRATING=true npx payload migrate` |\n| Prisma | `npx prisma db push` | `npx prisma db push --accept-data-loss` (if destructive) |\n| Drizzle | `npx drizzle-kit push` | `npx drizzle-kit push` |\n| Supabase | `supabase db push` | Set `SUPABASE_ACCESS_TOKEN` env var |\n| TypeORM | `npx typeorm migration:run` | `npx typeorm migration:run -d src/data-source.ts` |\n\nInject the following into the planner prompt (step 8) as an additional constraint:\n\n```markdown\n<schema_push_requirement>\n**[BLOCKING] Schema Push Required**\n\nThis phase modifies schema-relevant files ({detected_files}). The planner MUST include\na `[BLOCKING]` task that runs the database schema push command AFTER all schema file\nmodifications are complete but BEFORE verification.\n\n- ORM detected: {SCHEMA_ORM}\n- Push command: {push_command}\n- Non-TTY workaround: {env_hint}\n- If push requires interactive prompts that cannot be suppressed, flag the task for\n manual intervention with `autonomous: false`\n\nThis task is mandatory — the phase CANNOT pass verification without it. Build and\ntype checks will pass without the push (types come from config, not the live database),\ncreating a false-positive verification state.\n</schema_push_requirement>\n```\n\nDisplay: `Schema files detected ({SCHEMA_ORM}) — [BLOCKING] push task will be injected into plans`\n\n**If no schema-relevant files detected:** Skip silently.\n"
|
|
1565
1636
|
},
|
|
1566
1637
|
"produces": [],
|
|
1567
1638
|
"consumes": [
|
|
@@ -1915,6 +1986,7 @@ const bySkill = {
|
|
|
1915
1986
|
"mempalace-recall": "mempalace",
|
|
1916
1987
|
"mempalace-capture": "mempalace",
|
|
1917
1988
|
"validate-phase": "nyquist",
|
|
1989
|
+
"pareto-sync-board": "pareto-stack",
|
|
1918
1990
|
"profile-user": "profile-pipeline",
|
|
1919
1991
|
"secure-phase": "security",
|
|
1920
1992
|
"ui-phase": "ui",
|
|
@@ -2083,7 +2155,7 @@ const byLoopPoint = {
|
|
|
2083
2155
|
"into": "planner",
|
|
2084
2156
|
"fragment": {
|
|
2085
2157
|
"path": "fragments/plan-pre.md",
|
|
2086
|
-
"inline": "# Schema Push Detection Gate\n\n> Detects schema-relevant files in the phase scope and injects a mandatory `[BLOCKING]` schema push task into the plan. Prevents false-positive verification where build/types pass because TypeScript types come from config, not the live database.\n\nCheck if any files in the phase scope match schema patterns:\n\n```bash\nPHASE_SECTION=$(
|
|
2158
|
+
"inline": "# Schema Push Detection Gate\n\n> Detects schema-relevant files in the phase scope and injects a mandatory `[BLOCKING]` schema push task into the plan. Prevents false-positive verification where build/types pass because TypeScript types come from config, not the live database.\n\nCheck if any files in the phase scope match schema patterns:\n\n```bash\nPHASE_SECTION=$(pp_run query roadmap.get-phase \"${PHASE}\" --pick section 2>/dev/null)\n```\n\nScan `PHASE_SECTION`, `CONTEXT.md` (if loaded), and `RESEARCH.md` (if exists) for file paths matching these ORM patterns:\n\n| ORM | File Patterns |\n|-----|--------------|\n| Payload CMS | `src/collections/**/*.ts`, `src/globals/**/*.ts` |\n| Prisma | `prisma/schema.prisma`, `prisma/schema/*.prisma` |\n| Drizzle | `drizzle/schema.ts`, `src/db/schema.ts`, `drizzle/*.ts` |\n| Supabase | `supabase/migrations/*.sql` |\n| TypeORM | `src/entities/**/*.ts`, `src/migrations/**/*.ts` |\n\nAlso check if any existing PLAN.md files for this phase already reference these file patterns in `files_modified`.\n\n**If schema-relevant files detected:**\n\nSet `SCHEMA_PUSH_REQUIRED=true` and `SCHEMA_ORM={detected_orm}`.\n\nDetermine the push command for the detected ORM:\n\n| ORM | Push Command | Non-TTY Workaround |\n|-----|-------------|-------------------|\n| Payload CMS | `npx payload migrate` | `CI=true PAYLOAD_MIGRATING=true npx payload migrate` |\n| Prisma | `npx prisma db push` | `npx prisma db push --accept-data-loss` (if destructive) |\n| Drizzle | `npx drizzle-kit push` | `npx drizzle-kit push` |\n| Supabase | `supabase db push` | Set `SUPABASE_ACCESS_TOKEN` env var |\n| TypeORM | `npx typeorm migration:run` | `npx typeorm migration:run -d src/data-source.ts` |\n\nInject the following into the planner prompt (step 8) as an additional constraint:\n\n```markdown\n<schema_push_requirement>\n**[BLOCKING] Schema Push Required**\n\nThis phase modifies schema-relevant files ({detected_files}). The planner MUST include\na `[BLOCKING]` task that runs the database schema push command AFTER all schema file\nmodifications are complete but BEFORE verification.\n\n- ORM detected: {SCHEMA_ORM}\n- Push command: {push_command}\n- Non-TTY workaround: {env_hint}\n- If push requires interactive prompts that cannot be suppressed, flag the task for\n manual intervention with `autonomous: false`\n\nThis task is mandatory — the phase CANNOT pass verification without it. Build and\ntype checks will pass without the push (types come from config, not the live database),\ncreating a false-positive verification state.\n</schema_push_requirement>\n```\n\nDisplay: `Schema files detected ({SCHEMA_ORM}) — [BLOCKING] push task will be injected into plans`\n\n**If no schema-relevant files detected:** Skip silently.\n"
|
|
2087
2159
|
},
|
|
2088
2160
|
"produces": [],
|
|
2089
2161
|
"consumes": [
|
|
@@ -2330,6 +2402,26 @@ const byLoopPoint = {
|
|
|
2330
2402
|
"steps": [],
|
|
2331
2403
|
"contributions": [],
|
|
2332
2404
|
"gates": [
|
|
2405
|
+
{
|
|
2406
|
+
"capId": "pareto-stack",
|
|
2407
|
+
"point": "ship:pre",
|
|
2408
|
+
"check": {
|
|
2409
|
+
"query": "ship.platform-drift"
|
|
2410
|
+
},
|
|
2411
|
+
"when": "pareto.enabled",
|
|
2412
|
+
"blocking": true,
|
|
2413
|
+
"onError": "skip"
|
|
2414
|
+
},
|
|
2415
|
+
{
|
|
2416
|
+
"capId": "pareto-stack",
|
|
2417
|
+
"point": "ship:pre",
|
|
2418
|
+
"check": {
|
|
2419
|
+
"query": "ship.platform-drift"
|
|
2420
|
+
},
|
|
2421
|
+
"when": "pareto.enabled",
|
|
2422
|
+
"blocking": false,
|
|
2423
|
+
"onError": "skip"
|
|
2424
|
+
},
|
|
2333
2425
|
{
|
|
2334
2426
|
"capId": "security",
|
|
2335
2427
|
"point": "ship:pre",
|
|
@@ -2361,6 +2453,17 @@ const byLoopPoint = {
|
|
|
2361
2453
|
],
|
|
2362
2454
|
"when": "mempalace.enabled",
|
|
2363
2455
|
"onError": "skip"
|
|
2456
|
+
},
|
|
2457
|
+
{
|
|
2458
|
+
"capId": "pareto-stack",
|
|
2459
|
+
"point": "ship:post",
|
|
2460
|
+
"ref": {
|
|
2461
|
+
"skill": "pareto-sync-board"
|
|
2462
|
+
},
|
|
2463
|
+
"produces": [],
|
|
2464
|
+
"consumes": [],
|
|
2465
|
+
"when": "pareto.enabled",
|
|
2466
|
+
"onError": "skip"
|
|
2364
2467
|
}
|
|
2365
2468
|
],
|
|
2366
2469
|
"contributions": [],
|
|
@@ -2389,6 +2492,8 @@ const configKeys = {
|
|
|
2389
2492
|
"mempalace.diary_journal": "mempalace",
|
|
2390
2493
|
"mempalace.auto_capture_hooks": "mempalace",
|
|
2391
2494
|
"workflow.nyquist_validation": "nyquist",
|
|
2495
|
+
"pareto.enabled": "pareto-stack",
|
|
2496
|
+
"pareto.drift_block_on": "pareto-stack",
|
|
2392
2497
|
"workflow.pattern_mapper": "pattern-mapper",
|
|
2393
2498
|
"profile-pipeline.enabled": "profile-pipeline",
|
|
2394
2499
|
"workflow.research": "research",
|
|
@@ -2537,6 +2642,24 @@ const configSchema = {
|
|
|
2537
2642
|
"default": true,
|
|
2538
2643
|
"description": "Enable Nyquist validation coverage auditing."
|
|
2539
2644
|
},
|
|
2645
|
+
"pareto.enabled": {
|
|
2646
|
+
"owner": "pareto-stack",
|
|
2647
|
+
"type": "boolean",
|
|
2648
|
+
"default": false,
|
|
2649
|
+
"description": "Enable the Pareto stack capability. When false, all gates and steps are inert."
|
|
2650
|
+
},
|
|
2651
|
+
"pareto.drift_block_on": {
|
|
2652
|
+
"owner": "pareto-stack",
|
|
2653
|
+
"type": "enum",
|
|
2654
|
+
"default": "major",
|
|
2655
|
+
"description": "Minimum drift severity that blocks the ship gate. breaking is reserved and aliased to major.",
|
|
2656
|
+
"values": [
|
|
2657
|
+
"breaking",
|
|
2658
|
+
"major",
|
|
2659
|
+
"minor",
|
|
2660
|
+
"none"
|
|
2661
|
+
]
|
|
2662
|
+
},
|
|
2540
2663
|
"workflow.pattern_mapper": {
|
|
2541
2664
|
"owner": "pattern-mapper",
|
|
2542
2665
|
"type": "boolean",
|
|
@@ -3588,6 +3711,9 @@ const capabilityClusters = {
|
|
|
3588
3711
|
"nyquist": [
|
|
3589
3712
|
"validate-phase"
|
|
3590
3713
|
],
|
|
3714
|
+
"pareto-stack": [
|
|
3715
|
+
"pareto-sync-board"
|
|
3716
|
+
],
|
|
3591
3717
|
"profile-pipeline": [
|
|
3592
3718
|
"profile-user"
|
|
3593
3719
|
],
|
|
@@ -3637,6 +3763,12 @@ const profileMembership = {
|
|
|
3637
3763
|
"full"
|
|
3638
3764
|
]
|
|
3639
3765
|
},
|
|
3766
|
+
"pareto-stack": {
|
|
3767
|
+
"tier": "full",
|
|
3768
|
+
"profiles": [
|
|
3769
|
+
"full"
|
|
3770
|
+
]
|
|
3771
|
+
},
|
|
3640
3772
|
"profile-pipeline": {
|
|
3641
3773
|
"tier": "full",
|
|
3642
3774
|
"profiles": [
|
|
@@ -3681,6 +3813,7 @@ const _requiresGraph = {
|
|
|
3681
3813
|
"mempalace": [],
|
|
3682
3814
|
"nyquist": [],
|
|
3683
3815
|
"opencode": [],
|
|
3816
|
+
"pareto-stack": [],
|
|
3684
3817
|
"pattern-mapper": [
|
|
3685
3818
|
"research"
|
|
3686
3819
|
],
|
|
@@ -23,6 +23,10 @@ const phaseLocatorMod = require("./phase-locator.cjs");
|
|
|
23
23
|
const { findPhaseInternal } = phaseLocatorMod;
|
|
24
24
|
const decisions_cjs_1 = require("./decisions.cjs");
|
|
25
25
|
const ui_safety_gate_cjs_1 = require("./ui-safety-gate.cjs");
|
|
26
|
+
const semver_compare_cjs_1 = require("./semver-compare.cjs");
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
|
+
const projectRootMod = require("./project-root.cjs");
|
|
29
|
+
const { findProjectRoot } = projectRootMod;
|
|
26
30
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
31
|
const verifyModule = require("./verify.cjs");
|
|
28
32
|
const { cmdVerifySchemaDrift, cmdVerifyCodebaseDrift } = verifyModule;
|
|
@@ -727,6 +731,176 @@ function cmdGapAnalysisPlanPost(projectDir, args, raw) {
|
|
|
727
731
|
message: result.table || result.summary || '',
|
|
728
732
|
}, raw, undefined);
|
|
729
733
|
}
|
|
734
|
+
const DRIFT_SEVERITY_ORDINAL = {
|
|
735
|
+
none: 0,
|
|
736
|
+
minor: 1,
|
|
737
|
+
major: 2,
|
|
738
|
+
breaking: 3,
|
|
739
|
+
};
|
|
740
|
+
/**
|
|
741
|
+
* Classify drift severity from pinned vs latest semver tuples.
|
|
742
|
+
*
|
|
743
|
+
* Pre-condition: compareSemverCore(pinned, latest) < 0 (pinned is strictly behind).
|
|
744
|
+
* Severity mapping (A3):
|
|
745
|
+
* differing major → 'major' (breaking aliased to major; semver can't distinguish)
|
|
746
|
+
* equal major, differing minor → 'minor'
|
|
747
|
+
* equal major+minor, differing patch → 'none' (below minor; always warn, not block)
|
|
748
|
+
*/
|
|
749
|
+
function classifyDriftSeverity(pinned, latest) {
|
|
750
|
+
const [pMaj, pMin] = (0, semver_compare_cjs_1.toNumericTuple)(pinned);
|
|
751
|
+
const [lMaj, lMin] = (0, semver_compare_cjs_1.toNumericTuple)(latest);
|
|
752
|
+
if (lMaj > pMaj)
|
|
753
|
+
return 'major';
|
|
754
|
+
if (lMin > pMin)
|
|
755
|
+
return 'minor';
|
|
756
|
+
return 'none'; // patch-level difference
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Read pareto.drift_block_on from .planning/config.json.
|
|
760
|
+
* Returns 'major' (default) when config is absent, malformed, or the key is unset.
|
|
761
|
+
*/
|
|
762
|
+
function readParetoConfig(projectDir) {
|
|
763
|
+
const configPath = node_path_1.default.join(projectDir, '.planning', 'config.json');
|
|
764
|
+
try {
|
|
765
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
766
|
+
const pareto = parsed['pareto'] ?? {};
|
|
767
|
+
const raw = pareto['drift_block_on'];
|
|
768
|
+
if (typeof raw === 'string' && raw in DRIFT_SEVERITY_ORDINAL) {
|
|
769
|
+
return { drift_block_on: raw };
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
catch { /* config absent or malformed → use default */ }
|
|
773
|
+
return { drift_block_on: 'major' };
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Resolve the latest stable tag for a platform repo via git ls-remote.
|
|
777
|
+
*
|
|
778
|
+
* T-10-04: every candidate tag is filtered through isStableTripletSemver — never shell-eval.
|
|
779
|
+
* Returns '' when offline or the remote is unreachable (callers treat this as "unknown latest").
|
|
780
|
+
*/
|
|
781
|
+
function resolveLatestTagViaGitRemote(repoShortPath) {
|
|
782
|
+
// Construct GitLab URL from "group/project" format (e.g. "pareto4/pareto-platform-gcp").
|
|
783
|
+
const remoteUrl = `https://gitlab.com/${repoShortPath}`;
|
|
784
|
+
try {
|
|
785
|
+
const lsOutput = (0, node_child_process_1.execFileSync)('git', ['ls-remote', '--tags', '--refs', '--sort=-v:refname', remoteUrl], { encoding: 'utf-8', timeout: 10_000, maxBuffer: 512 * 1024, windowsHide: true });
|
|
786
|
+
// Sort is descending by version (--sort=-v:refname) so the first isStableTripletSemver match
|
|
787
|
+
// is the latest stable tag. T-10-04: never use an unfiltered tag string.
|
|
788
|
+
const tag = lsOutput
|
|
789
|
+
.split('\n')
|
|
790
|
+
.map((line) => {
|
|
791
|
+
const m = line.match(/\brefs\/tags\/([^\s^]+)$/);
|
|
792
|
+
return m ? m[1].trim() : '';
|
|
793
|
+
})
|
|
794
|
+
.find((t) => t && (0, semver_compare_cjs_1.isStableTripletSemver)(t));
|
|
795
|
+
return tag ?? '';
|
|
796
|
+
}
|
|
797
|
+
catch {
|
|
798
|
+
return ''; // offline or unreachable → signals "unknown latest" (Pitfall 4)
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* ship.platform-drift query handler (HOOK-02).
|
|
803
|
+
*
|
|
804
|
+
* Derives a graded severity verdict from the pinned-vs-latest platform tag diff,
|
|
805
|
+
* maps it to the pareto.drift_block_on threshold, and degrades safely:
|
|
806
|
+
* - Inert when .platform-bindings.json is absent (HOOK-04 runtime file layer).
|
|
807
|
+
* - Non-blocking warn when latest is unresolved or offline (T-10-06, Pitfall 4).
|
|
808
|
+
*
|
|
809
|
+
* Verdict shape: { status: 'inert'|'pass'|'warn'|'block', block, severity, pinned, latest, present }
|
|
810
|
+
* block (boolean) mirrors status === 'block' — the uniform gate contract field the host
|
|
811
|
+
* loop reads regardless of which capability owns the gate.
|
|
812
|
+
*
|
|
813
|
+
* Invocable as: pp_run check ship.platform-drift [--latest-tag <tag>] [--raw]
|
|
814
|
+
* The dot → hyphen normalization in routeCheckCommand maps "ship.platform-drift"
|
|
815
|
+
* to the "ship-platform-drift" branch.
|
|
816
|
+
*
|
|
817
|
+
* Reuse:
|
|
818
|
+
* - Phase 7 reader contract: reads .platform-bindings.json directly at repo root
|
|
819
|
+
* (same find/parse/TOCTOU pattern as cmdValidateBindings in platform-bindings.cts).
|
|
820
|
+
* - semver-compare.cjs: toNumericTuple, compareSemverCore, isStableTripletSemver
|
|
821
|
+
* (no reimplemented semver; no shell-eval of tag strings).
|
|
822
|
+
*/
|
|
823
|
+
function cmdShipPlatformDrift(cwd, args, raw) {
|
|
824
|
+
// Step 1: Resolve project root (same approach as cmdValidateBindings in platform-bindings.cts).
|
|
825
|
+
let root;
|
|
826
|
+
try {
|
|
827
|
+
root = findProjectRoot(cwd) || cwd;
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
root = cwd;
|
|
831
|
+
}
|
|
832
|
+
// Step 1b: HOOK-04 runtime file layer — absent → inert (never error(), opt-in default).
|
|
833
|
+
const bindingsPath = node_path_1.default.join(root, '.platform-bindings.json');
|
|
834
|
+
if (!node_fs_1.default.existsSync(bindingsPath)) {
|
|
835
|
+
output({ status: 'inert', block: false, present: false }, raw, undefined);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
// Step 2: Parse .platform-bindings.json (malformed structure → graceful warn, not block).
|
|
839
|
+
let bindingsObj;
|
|
840
|
+
try {
|
|
841
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(bindingsPath, 'utf-8'));
|
|
842
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
843
|
+
output({
|
|
844
|
+
status: 'warn', block: false, severity: 'none',
|
|
845
|
+
pinned: null, latest: null, present: true,
|
|
846
|
+
message: 'Malformed .platform-bindings.json structure — drift check skipped',
|
|
847
|
+
}, raw, undefined);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
bindingsObj = parsed;
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
// TOCTOU guard — file vanished between existsSync and readFileSync → inert path.
|
|
854
|
+
output({ status: 'inert', block: false, present: false }, raw, undefined);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
// Extract platform.tag (PINNED) and platform.repo for remote tag resolution.
|
|
858
|
+
const platformField = (typeof bindingsObj['platform'] === 'object' &&
|
|
859
|
+
bindingsObj['platform'] !== null &&
|
|
860
|
+
!Array.isArray(bindingsObj['platform'])) ? bindingsObj['platform'] : null;
|
|
861
|
+
const pinned = typeof platformField?.['tag'] === 'string' ? platformField['tag'].trim() : '';
|
|
862
|
+
const platformRepo = typeof platformField?.['repo'] === 'string' ? platformField['repo'].trim() : '';
|
|
863
|
+
// Guard: missing or invalid pinned tag → advisory warn, do not block.
|
|
864
|
+
if (!pinned || !(0, semver_compare_cjs_1.isStableTripletSemver)(pinned)) {
|
|
865
|
+
output({
|
|
866
|
+
status: 'warn', block: false, severity: 'none',
|
|
867
|
+
pinned: pinned || null, latest: null, present: true,
|
|
868
|
+
message: 'platform.tag is missing or not a valid semver in .platform-bindings.json — drift check advisory only',
|
|
869
|
+
}, raw, undefined);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
// Step 3: Resolve LATEST via --latest-tag arg (deterministic) or git ls-remote (live).
|
|
873
|
+
const latestTagArgIdx = args.indexOf('--latest-tag');
|
|
874
|
+
let latest = latestTagArgIdx >= 0 ? (args[latestTagArgIdx + 1] ?? '') : '';
|
|
875
|
+
if (!latest && platformRepo) {
|
|
876
|
+
// T-10-04: resolveLatestTagViaGitRemote filters all tags through isStableTripletSemver.
|
|
877
|
+
latest = resolveLatestTagViaGitRemote(platformRepo);
|
|
878
|
+
}
|
|
879
|
+
// Step 4: Offline / unresolved latest → non-blocking warn (T-10-06, Pitfall 4).
|
|
880
|
+
// A disconnected dev or CI run must never be blocked by an unresolvable remote.
|
|
881
|
+
if (!latest || !(0, semver_compare_cjs_1.isStableTripletSemver)(latest)) {
|
|
882
|
+
output({
|
|
883
|
+
status: 'warn', block: false, severity: 'none',
|
|
884
|
+
pinned, latest: latest || null, present: true,
|
|
885
|
+
message: 'Latest platform tag could not be resolved (offline or unreachable remote) — drift check advisory only',
|
|
886
|
+
}, raw, undefined);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
// Step 4b: Pinned is current or ahead of latest → pass.
|
|
890
|
+
if ((0, semver_compare_cjs_1.compareSemverCore)(pinned, latest) >= 0) {
|
|
891
|
+
output({ status: 'pass', block: false, severity: 'none', pinned, latest, present: true }, raw, undefined);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
// Step 5: Classify drift severity from tuple diff (A3; breaking aliased to major for now).
|
|
895
|
+
const severity = classifyDriftSeverity(pinned, latest);
|
|
896
|
+
// Step 6: Read pareto.drift_block_on threshold (default 'major').
|
|
897
|
+
const { drift_block_on: threshold } = readParetoConfig(cwd);
|
|
898
|
+
const thresholdOrd = DRIFT_SEVERITY_ORDINAL[threshold] ?? DRIFT_SEVERITY_ORDINAL['major'];
|
|
899
|
+
const severityOrd = DRIFT_SEVERITY_ORDINAL[severity];
|
|
900
|
+
// Step 7: Emit verdict — block when severity ≥ threshold, warn otherwise.
|
|
901
|
+
const status = severityOrd >= thresholdOrd ? 'block' : 'warn';
|
|
902
|
+
output({ status, block: status === 'block', severity, pinned, latest, present: true }, raw, undefined);
|
|
903
|
+
}
|
|
730
904
|
function routeCheckCommand({ args, cwd, raw }) {
|
|
731
905
|
// Normalize dots to hyphens in the subcommand so both forms are accepted.
|
|
732
906
|
// This makes `check.query = "ui.plan-gate"` (dotted form in capability.json gates)
|
|
@@ -764,6 +938,13 @@ function routeCheckCommand({ args, cwd, raw }) {
|
|
|
764
938
|
cmdUiSafetyGate(cwd, args, raw);
|
|
765
939
|
return;
|
|
766
940
|
}
|
|
941
|
+
if (subcommand === 'ship-platform-drift') {
|
|
942
|
+
// ship.platform-drift — graded platform-tag drift check (HOOK-02).
|
|
943
|
+
// Dot-to-hyphen normalization means query "ship.platform-drift" routes here.
|
|
944
|
+
// Reuses Phase 7 reader contract + semver-compare; inert when bindings absent (HOOK-04).
|
|
945
|
+
cmdShipPlatformDrift(cwd, args, raw);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
767
948
|
if (subcommand === 'verify-schema-drift') {
|
|
768
949
|
// Delegates to verify.schema-drift — drift capability gate at execute:wave:post (blocking).
|
|
769
950
|
// Dot-to-hyphen normalization means query "verify.schema-drift" routes here.
|
|
@@ -787,7 +968,7 @@ function routeCheckCommand({ args, cwd, raw }) {
|
|
|
787
968
|
(0, prohibition_enforcement_cjs_1.routeProhibitionEnforcement)(args, raw);
|
|
788
969
|
return;
|
|
789
970
|
}
|
|
790
|
-
error('Unknown check subcommand. Available: auto-mode, decision-coverage-plan, decision-coverage-verify, gap-analysis-plan-post, prohibition-enforcement, tdd-review-checkpoint, ui-plan-gate, ui-safety-gate, verify-schema-drift, verify-codebase-drift', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
971
|
+
error('Unknown check subcommand. Available: auto-mode, decision-coverage-plan, decision-coverage-verify, gap-analysis-plan-post, prohibition-enforcement, ship-platform-drift, tdd-review-checkpoint, ui-plan-gate, ui-safety-gate, verify-schema-drift, verify-codebase-drift', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
791
972
|
}
|
|
792
973
|
module.exports = {
|
|
793
974
|
routeCheckCommand,
|
|
@@ -797,4 +978,5 @@ module.exports = {
|
|
|
797
978
|
computeUiSafetyGate,
|
|
798
979
|
cmdGapAnalysisPlanPost,
|
|
799
980
|
cmdTddReviewCheckpoint,
|
|
981
|
+
cmdShipPlatformDrift,
|
|
800
982
|
};
|
|
@@ -583,6 +583,13 @@ exports.NON_FAMILY_COMMAND_ALIASES = [
|
|
|
583
583
|
],
|
|
584
584
|
"mutation": false
|
|
585
585
|
},
|
|
586
|
+
{
|
|
587
|
+
"canonical": "check.ship-platform-drift",
|
|
588
|
+
"aliases": [
|
|
589
|
+
"check ship-platform-drift"
|
|
590
|
+
],
|
|
591
|
+
"mutation": false
|
|
592
|
+
},
|
|
586
593
|
{
|
|
587
594
|
"canonical": "commit",
|
|
588
595
|
"aliases": [],
|
package/pp-core/bin/lib/io.cjs
CHANGED
|
@@ -175,6 +175,8 @@ const ERROR_REASON = Object.freeze({
|
|
|
175
175
|
HOOKS_OPT_OUT: 'hooks_opt_out',
|
|
176
176
|
// security-scan
|
|
177
177
|
SECURITY_SCAN_FAILED: 'security_scan_failed',
|
|
178
|
+
// platform-bindings
|
|
179
|
+
PLATFORM_BINDINGS_PARSE_FAILED: 'platform_bindings_parse_failed',
|
|
178
180
|
// generic
|
|
179
181
|
USAGE: 'usage',
|
|
180
182
|
UNKNOWN: 'unknown',
|